home / skills / amnadtaowsoam / cerebraskills / api-design-contracts

This skill helps you design robust APIs with contract-first OpenAPI/AsyncAPI, enabling backward compatibility, contract testing, and automatic client

npx playbooks add skill amnadtaowsoam/cerebraskills --skill api-design-contracts

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
9.9 KB
---
name: API Design Contracts
description: API contract-first design using OpenAPI/Swagger, AsyncAPI for events, versioning strategies, backward compatibility, and contract testing
---

# API Design Contracts

## Overview

API contract-first design using OpenAPI/Swagger for REST and AsyncAPI for events to create clear contracts, support backward compatibility, and enable contract testing between services

## Why This Matters

- **Contract-first**: Design API before implementation, reduce rework
- **Type safety**: Auto-generate clients from contract
- **Backward compatibility**: Clear versioning strategy
- **Contract testing**: Detect breaking changes early

---

## Core Concepts

### 1. OpenAPI Specification

```yaml
openapi: 3.0.3
info:
  title: Example API
  version: 1.0.0
  description: API contract example
servers:
  - url: https://api.example.com/v1
    description: Production server

paths:
  /users:
    get:
      summary: List users
      operationId: listUsers
      tags:
        - users
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
    post:
      summary: Create user
      operationId: createUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserRequest'
      responses:
        '201':
          description: User created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'

components:
  schemas:
    User:
      type: object
      required:
        - id
        - email
        - name
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        name:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    CreateUserRequest:
      type: object
      required:
        - email
        - name
      properties:
        email:
          type: string
          format: email
        name:
          type: string
          minLength: 1
          maxLength: 100

    Pagination:
      type: object
      properties:
        cursor:
          type: string
        hasMore:
          type: boolean
```

### 2. AsyncAPI Specification

```yaml
asyncapi: '2.6.0'
info:
  title: User Events API
  version: '1.0.0'
  description: Events for user lifecycle

servers:
  production:
    url: broker.example.com
    protocol: kafka
    description: Production Kafka broker

channels:
  user.created:
    publish:
      summary: User created event
      message:
        name: UserCreated
        payload:
          type: object
          required:
            - userId
            - email
            - name
            - timestamp
          properties:
            userId:
              type: string
              format: uuid
            email:
              type: string
              format: email
            name:
              type: string
            timestamp:
              type: string
              format: date-time

  user.updated:
    publish:
      summary: User updated event
      message:
        name: UserUpdated
        payload:
          type: object
          required:
            - userId
            - timestamp
          properties:
            userId:
              type: string
              format: uuid
            changes:
              type: object
              additionalProperties: true
            timestamp:
              type: string
              format: date-time

components:
  schemas:
    UserCreated:
      type: object
      required:
        - userId
        - email
        - name
        - timestamp
      properties:
        userId:
          type: string
          format: uuid
        email:
          type: string
          format: email
        name:
          type: string
        timestamp:
          type: string
          format: date-time
```

### 3. Versioning Strategy

**URL Versioning (Recommended):**
```yaml
servers:
  - url: https://api.example.com/v1
    description: Version 1 (current)
  - url: https://api.example.com/v2
    description: Version 2 (new features)
```

**Header Versioning:**
```yaml
paths:
  /users:
    get:
      parameters:
        - name: API-Version
          in: header
          schema:
            type: string
            enum: ['1.0', '2.0']
            default: '1.0'
```

**Versioning Rules:**
- Breaking changes = new major version
- Additive changes = same version
- Deprecation period = minimum 6 months
- Sunset policy = documented and communicated

### 4. Backward Compatibility

**Additive Changes (Safe):**
```yaml
# Add new optional field
properties:
  email:
    type: string
  phone:  # New optional field
    type: string
```

**Breaking Changes (New Version):**
```yaml
# Change field type
properties:
  userId:
    type: string  # Changed from integer
```

**Deprecation Pattern:**
```yaml
paths:
  /users:
    get:
      deprecated: true
      x-deprecation-date: '2026-06-01'
      x-sunset-date: '2026-12-01'
      x-migration-guide: 'https://docs.example.com/migration/v1-to-v2'
```

### 5. Contract Testing

**Pact (Consumer-Driven Contracts):**
```typescript
// Consumer test
import { Pact } from '@pact-foundation/pact'
import { expect } from 'chai'

describe('User API Consumer', () => {
  const provider = new Pact({
    consumer: 'frontend-app',
    provider: 'user-service',
    port: 1234,
  })

  before(() => provider.setup())
  after(() => provider.finalize())

  it('should get user list', async () => {
    await provider.addInteraction({
      state: 'users exist',
      uponReceiving: 'a request for users',
      withRequest: {
        method: 'GET',
        path: '/api/v1/users',
        query: { limit: '20' },
      },
      willRespondWith: {
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: {
          data: eachLike({
            id: like('123e4567-e89b-12d3-a456-426614174000'),
            email: like('[email protected]'),
            name: like('John Doe'),
          }),
          pagination: {
            cursor: like('abc123'),
            hasMore: false,
          },
        },
      },
    })

    const response = await fetch('http://localhost:1234/api/v1/users?limit=20')
    const data = await response.json()

    expect(data.data).to.be.an('array')
  })
})
```

**Provider Verification:**
```typescript
// Provider verification
import { Verifier } from '@pact-foundation/pact'

describe('User API Provider', () => {
  it('should validate consumer contracts', async () => {
    const verifier = new Verifier({
      providerBaseUrl: 'http://localhost:3000',
      pactUrls: ['./pacts/frontend-app-user-service.json'],
    })

    await verifier.verify()
  })
})
```

### 6. Schema Validation

**Request Validation:**
```typescript
import Ajv from 'ajv'

const ajv = new Ajv()

// Validate request against OpenAPI schema
function validateRequest(schema: any, data: any) {
  const validate = ajv.compile(schema)
  const valid = validate(data)

  if (!valid) {
    throw new Error(`Validation failed: ${JSON.stringify(validate.errors)}`)
  }

  return data
}

// Usage
const createUserSchema = {
  type: 'object',
  required: ['email', 'name'],
  properties: {
    email: { type: 'string', format: 'email' },
    name: { type: 'string', minLength: 1, maxLength: 100 },
  },
}

validateRequest(createUserSchema, requestBody)
```

### 7. Code Generation

**Generate TypeScript Types:**
```bash
# Using openapi-typescript-codegen
npx openapi-typescript-codegen -i openapi.yaml -o ./src/api
```

**Generated Types:**
```typescript
// Auto-generated from OpenAPI
export interface User {
  id: string
  email: string
  name: string
  createdAt: string
  updatedAt: string
}

export interface CreateUserRequest {
  email: string
  name: string
}

export interface PaginatedResponse<T> {
  data: T[]
  pagination: {
    cursor: string
    hasMore: boolean
  }
}
```

## Quick Start

1. **Create OpenAPI spec:**
```bash
npm install -g @apidevtools/swagger-cli
swagger-cli validate openapi.yaml
```

2. **Generate types:**
```bash
npx openapi-typescript openapi.yaml -o src/api/types.ts
```

3. **Set up contract testing:**
```bash
npm install --save-dev @pact-foundation/pact
```

4. **Generate API client:**
```bash
npx openapi-typescript-codegen -i openapi.yaml -o ./src/api
```

## Production Checklist

- [ ] OpenAPI/AsyncAPI spec exists and is valid
- [ ] Versioning strategy documented
- [ ] Backward compatibility guidelines defined
- [ ] Contract tests implemented
- [ ] Schema validation in place
- [ ] Type generation automated
- [ ] Deprecation policy documented
- [ ] Breaking change detection in CI

## Anti-patterns

1. **Code-first without contract**: Implement API first, create contract later
2. **No versioning**: Breaking changes break clients
3. **Ignoring deprecation**: Delete old version without advance notice
4. **Loose contracts**: Unclear schema loses type safety
5. **No contract testing**: Breaking changes leak to production

## Integration Points

- API gateways
- Client SDK generators
- Contract testing frameworks
- CI/CD pipelines
- Documentation tools

## Further Reading

- [OpenAPI Specification](https://swagger.io/specification/)
- [AsyncAPI Specification](https://www.asyncapi.com/docs/specifications/latest)
- [Pact Contract Testing](https://docs.pact.io/)
- [API Versioning Best Practices](https://restfulapi.net/versioning/)

Overview

This skill implements API contract-first design using OpenAPI/Swagger for REST and AsyncAPI for event-driven interfaces. It codifies versioning strategies, backward compatibility rules, and contract testing patterns to prevent breaking changes and accelerate client generation. The outcome is reliable, type-safe APIs and automated checks throughout CI/CD.

How this skill works

Define API surface as OpenAPI and event schemas as AsyncAPI before writing implementation. Use schema-driven validation and code generation to produce clients, types, and server stubs. Add version metadata, deprecation annotations, and contract tests (consumer and provider) to detect regressions and enforce compatibility during CI runs.

When to use it

  • Designing new public or internal REST or event APIs where client stability matters
  • When multiple teams depend on the same service and backward compatibility is required
  • You need automated client SDKs or type generation to reduce runtime errors
  • Introducing a formal versioning and deprecation policy for existing APIs
  • Integrating contract testing into CI to prevent breaking changes

Best practices

  • Start contract-first: author OpenAPI/AsyncAPI before implementation to reduce rework
  • Prefer URL versioning for clarity; use header versioning only when necessary
  • Treat additive schema changes as safe; route breaking changes to a new major version
  • Document deprecation dates, migration guides, and a minimum six-month deprecation period
  • Automate schema validation, codegen, and contract tests in CI/CD pipelines

Example use cases

  • Generate type-safe client libraries from OpenAPI to keep frontend and backend consistent
  • Publish AsyncAPI event contracts for consumers to subscribe safely to user lifecycle events
  • Add Pact consumer tests in frontend suites and run provider verification during service builds
  • Enforce backwards-compatible changes by validating new specs against previous versions in CI
  • Roll out a v2 API with documented migration guide and sunset policy for v1

FAQ

How do I decide between URL and header versioning?

Prefer URL versioning for public APIs because it is explicit and easy to cache; use header versioning when you need per-request negotiation without changing endpoints.

What counts as a breaking change?

Removing fields, changing field types, renaming endpoints, or altering required properties are breaking. Additive, optional fields are non-breaking.