api-contract

Contract-First API design — write OpenAPI 3.1 or AsyncAPI 3.0 specs before implementation, generate or validate code against them, detect breaking changes in CI, and apply Consumer-Driven Contract Testing with Pact.

8 stars

Best use case

api-contract is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Contract-First API design — write OpenAPI 3.1 or AsyncAPI 3.0 specs before implementation, generate or validate code against them, detect breaking changes in CI, and apply Consumer-Driven Contract Testing with Pact.

Teams using api-contract should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/api-contract/SKILL.md --create-dirs "https://raw.githubusercontent.com/marvinrichter/clarc/main/skills/api-contract/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/api-contract/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How api-contract Compares

Feature / Agentapi-contractStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Contract-First API design — write OpenAPI 3.1 or AsyncAPI 3.0 specs before implementation, generate or validate code against them, detect breaking changes in CI, and apply Consumer-Driven Contract Testing with Pact.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Contract-First API Design

The spec is the source of truth. Code is generated from it — or validated against it. Never the other way around.

## When to Activate

- Starting any new API endpoint, service interface, or event schema
- Adding or changing a contract between two services
- Detecting whether a change is breaking for consumers
- Setting up API CI pipelines (lint, validate, diff)
- Integrating Consumer-Driven Contract Testing (Pact)
- Replacing a code-first API with a proper contract

---

## Core Principle

```plantuml
@startuml
|Code-First (WRONG)|
start
:write implementation code;
:generate spec\nfrom annotations / reflection;
:spec drifts\nas code evolves;
:consumers\ncannot trust the docs;
stop

|Contract-First (RIGHT)|
start
:write openapi.yaml\n(spec first);
:lint with spectral;
:generate types / stubs\nfrom spec;
:implement business\nlogic only;
:CI validates impl\nagainst spec;
stop
@enduml
```

The spec defines the **public interface**. Implementation details are private.

---

## Step 1: Write the Spec First

### OpenAPI 3.1 Minimal Structure

```yaml
# api/v1/openapi.yaml
openapi: "3.1.0"
info:
  title: Orders API
  version: "1.0.0"

paths:
  /orders:
    post:
      operationId: createOrder
      summary: Create a new order
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateOrderRequest"
      responses:
        "201":
          description: Order created
          headers:
            Location:
              schema:
                type: string
              description: URL of the created order
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
        "400":
          $ref: "#/components/responses/ValidationError"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /orders/{orderId}:
    get:
      operationId: getOrder
      parameters:
        - name: orderId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
        "404":
          $ref: "#/components/responses/NotFound"

components:
  schemas:
    CreateOrderRequest:
      type: object
      required: [customerId, items]
      properties:
        customerId:
          type: string
          format: uuid
        items:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/OrderItem"

    OrderItem:
      type: object
      required: [productId, quantity]
      properties:
        productId:
          type: string
          format: uuid
        quantity:
          type: integer
          minimum: 1

    Order:
      type: object
      required: [id, customerId, status, items, createdAt]
      properties:
        id:
          type: string
          format: uuid
        customerId:
          type: string
          format: uuid
        status:
          type: string
          enum: [pending, confirmed, shipped, cancelled]
        items:
          type: array
          items:
            $ref: "#/components/schemas/OrderItem"
        createdAt:
          type: string
          format: date-time

  responses:
    ValidationError:
      description: Input validation failed
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
    UnprocessableEntity:
      description: Semantically invalid request
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
    NotFound:
      description: Resource not found
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"

    ProblemDetails:
      type: object
      required: [type, title, status]
      properties:
        type:
          type: string
          format: uri
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        instance:
          type: string
          format: uri
        errors:
          type: array
          items:
            type: object
            properties:
              field:
                type: string
              detail:
                type: string
```

### File Convention

```
api/
├── v1/
│   ├── openapi.yaml          # REST API contract
│   └── asyncapi.yaml         # Event/message contract (if applicable)
└── v2/
    └── openapi.yaml          # Breaking changes go in new version
```

The spec files live in version control alongside the code. They are the PR-reviewable contract.

---

## Step 2: Lint the Spec

Before generating or implementing anything, lint the spec with [Spectral](https://stoplight.io/open-source/spectral):

```bash
# Install
npm install -g @stoplight/spectral-cli

# Lint with built-in OpenAPI ruleset
spectral lint api/v1/openapi.yaml

# Lint with custom ruleset (enforce project conventions)
spectral lint api/v1/openapi.yaml --ruleset .spectral.yaml
```

### Custom Spectral Ruleset (`.spectral.yaml`)

```yaml
extends: ["spectral:oas"]
rules:
  # All operations must have operationId
  operation-operationId: error

  # All error responses must use application/problem+json
  error-response-content-type:
    message: Error responses must use application/problem+json
    given: "$.paths[*][*].responses[?(@property >= '400')].content"
    then:
      field: "application/problem+json"
      function: truthy

  # No inline schemas — use $ref
  no-inline-schemas:
    message: Use $ref instead of inline schemas
    given: "$.paths[*][*][responses,requestBody]..schema"
    then:
      field: "$ref"
      function: truthy
```

---

## Step 3: Generate Code from the Spec

```plantuml
@startuml
!include <C4/C4_Container>
System_Boundary(contract, "api/v1/") {
  Container(spec, "openapi.yaml", "OpenAPI 3.1", "Single source of truth — never generated from code")
}
System_Boundary(generators, "Code Generators") {
  Container(ts_gen, "openapi-typescript", "npx CLI", "TypeScript types")
  Container(go_gen, "oapi-codegen", "go CLI", "Go server interface + types")
  Container(java_gen, "openapi-generator", "Maven plugin", "Spring server stubs")
  Container(py_gen, "datamodel-codegen", "pip CLI", "Pydantic v2 models")
}
System_Boundary(services, "Service Implementations") {
  Container(ts_svc, "TypeScript Service", "Node.js", "implements business logic only")
  Container(go_svc, "Go Service", "net/http", "implements generated ServerInterface")
  Container(java_svc, "Spring Boot Service", "Java", "implements generated OrdersApi")
  Container(py_svc, "FastAPI Service", "Python", "uses generated Pydantic models")
}
Rel_D(spec, ts_gen, "generates")
Rel_D(spec, go_gen, "generates")
Rel_D(spec, java_gen, "generates")
Rel_D(spec, py_gen, "generates")
Rel_D(ts_gen, ts_svc, "consumed by")
Rel_D(go_gen, go_svc, "consumed by")
Rel_D(java_gen, java_svc, "consumed by")
Rel_D(py_gen, py_svc, "consumed by")
@enduml
```

### TypeScript / Node.js

```bash
# Generate typed client + server types
npx openapi-typescript api/v1/openapi.yaml -o src/generated/api.ts

# Or with @hey-api/openapi-ts (full SDK)
npx @hey-api/openapi-ts \
  --input api/v1/openapi.yaml \
  --output src/generated \
  --client fetch
```

Usage in TypeScript:
```typescript
// Use generated types — never write request/response types by hand
import type { paths } from "./generated/api"

type CreateOrderRequest = paths["/orders"]["post"]["requestBody"]["content"]["application/json"]
type Order = paths["/orders/{orderId}"]["get"]["responses"]["200"]["content"]["application/json"]
```

### Go

```bash
# Generate server interface + types with oapi-codegen
go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest

oapi-codegen \
  --config oapi-codegen.yaml \
  api/v1/openapi.yaml
```

`oapi-codegen.yaml`:
```yaml
package: api
generate:
  - types
  - server      # generates net/http or chi/echo handler interface
  - spec        # embeds spec for runtime validation
output: internal/api/generated.go
```

Implement only the interface — the routing and request parsing is generated:
```go
// Implement the generated ServerInterface — only business logic here
type OrderHandler struct{ svc *OrderService }

func (h *OrderHandler) CreateOrder(w http.ResponseWriter, r *http.Request) {
    // request already parsed and validated by generated middleware
}
```

### Java / Spring Boot

```bash
# Generate Spring server stubs
npx @openapitools/openapi-generator-cli generate \
  -i api/v1/openapi.yaml \
  -g spring \
  -o build/generated \
  --additional-properties=interfaceOnly=true,useSpringBoot3=true,useTags=true
```

`pom.xml` (Maven plugin — runs on build):
```xml
<plugin>
  <groupId>org.openapitools</groupId>
  <artifactId>openapi-generator-maven-plugin</artifactId>
  <version>7.x.x</version>
  <executions>
    <execution>
      <goals><goal>generate</goal></goals>
      <configuration>
        <inputSpec>${project.basedir}/api/v1/openapi.yaml</inputSpec>
        <generatorName>spring</generatorName>
        <configOptions>
          <interfaceOnly>true</interfaceOnly>
          <useSpringBoot3>true</useSpringBoot3>
        </configOptions>
      </configuration>
    </execution>
  </executions>
</plugin>
```

Implement only the interface:
```java
@RestController
public class OrderController implements OrdersApi {  // generated interface
    @Override
    public ResponseEntity<Order> createOrder(CreateOrderRequest req) {
        // business logic only — routing, validation, serialization: generated
    }
}
```

### Python / FastAPI

```bash
# Generate Pydantic models from spec
pip install datamodel-code-generator

datamodel-codegen \
  --input api/v1/openapi.yaml \
  --input-file-type openapi \
  --output src/generated/models.py \
  --output-model-type pydantic_v2.BaseModel
```

```python
from generated.models import CreateOrderRequest, Order

@router.post("/orders", response_model=Order, status_code=201)
async def create_order(request: CreateOrderRequest) -> Order:
    ...  # business logic only
```

---

## Step 4: Validate Implementation Against Spec

Running tests against the spec (not just unit tests):

### Schemathesis (property-based testing against spec)

```bash
pip install schemathesis

# Test a running server against its spec — finds contract violations automatically
schemathesis run api/v1/openapi.yaml \
  --base-url http://localhost:8080 \
  --checks all
```

Schemathesis generates test cases from the spec and verifies:
- All responses match declared schemas
- Status codes are correct
- Required fields are always present

### Dredd (example-based)

```bash
npm install -g dredd

dredd api/v1/openapi.yaml http://localhost:8080
```

---

## Step 5: Detect Breaking Changes in CI

A "breaking change" is any change that would break existing consumers without a version bump.

### Breaking vs. Non-Breaking

| Change | Breaking? |
|---|---|
| Remove a field from response | Yes |
| Rename a field | Yes |
| Change field type | Yes |
| Remove an endpoint | Yes |
| Add required request field | Yes |
| Add optional request field | No |
| Add new field to response | No |
| Add a new endpoint | No |
| Add a new enum value | Potentially (if consumer switches on enum) |

```plantuml
@startuml
start
fork
  :spectral lint\nopenapi.yaml;
  if (lint errors?) then (yes)
    :FAIL PR; <<#tomato>>
    detach
  endif
  :Lint OK;
fork again
  :oasdiff breaking\nmain vs PR branch;
  if (breaking changes\nwithout new version?) then (yes)
    :FAIL PR\n(bump to /v2/); <<#tomato>>
    detach
  endif
  :Breaking Change OK;
fork again
  :docker compose up;
  :schemathesis run\n--checks all;
  if (contract\nviolations?) then (yes)
    
    :FAIL PR; <<#tomato>>
    detach
  endif
  :Implementation OK;
end fork
:PR approved — merge allowed; <<#lightgreen>>
stop
@enduml
```

### oasdiff (CLI + CI)

```bash
# Install
go install github.com/tufin/oasdiff@latest

# Check for breaking changes between versions
oasdiff breaking api/v1/openapi-main.yaml api/v1/openapi-branch.yaml

# In CI (exit code 1 if breaking changes found)
oasdiff breaking \
  https://raw.githubusercontent.com/org/repo/main/api/v1/openapi.yaml \
  api/v1/openapi.yaml \
  --fail-on ERR
```

### GitHub Actions CI

```yaml
# .github/workflows/api-contract.yml
name: API Contract

on: [pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Lint OpenAPI spec
        run: npx @stoplight/spectral-cli lint api/v1/openapi.yaml

  breaking-changes:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Detect breaking changes
        uses: tufin/oasdiff-action@main
        with:
          base: "origin/main:api/v1/openapi.yaml"
          revision: "api/v1/openapi.yaml"
          fail-on-diff: true

  validate-implementation:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Start server
        run: docker compose up -d --wait
      - name: Run Schemathesis
        run: |
          pip install schemathesis
          schemathesis run api/v1/openapi.yaml \
            --base-url http://localhost:8080 \
            --checks all
```

---

## Steps 6-7: Event-Driven APIs and Contract Testing

For AsyncAPI 3.0 (Kafka, NATS, SQS, WebSocket) and Consumer-Driven Contract Testing with Pact, see `api-contract-advanced`.

---

## Contract-First Checklist

Before writing implementation code for any new API surface:

- [ ] Spec written in `api/v1/openapi.yaml` (or `asyncapi.yaml` for events)
- [ ] All schemas use `$ref` — no inline schemas
- [ ] All error responses reference `ProblemDetails` schema
- [ ] `operationId` set on every operation
- [ ] `spectral lint` passes with zero errors
- [ ] Code generated from spec (types, stubs, or interfaces)
- [ ] Implementation only touches business logic — not request/response shapes
- [ ] `schemathesis` or `dredd` runs in CI against live server
- [ ] `oasdiff` configured in CI to block breaking changes without version bump
- [ ] New version directory (`api/v2/`) created for any breaking change

---

## Anti-Patterns

### Generate Spec from Code
```
# BAD: spec becomes a reflection of implementation details
springdoc.api-docs.enabled=true   # auto-generates from annotations
```
The spec will drift the moment someone renames a method or adds an annotation. It becomes stale documentation, not a contract.

### Spec as Documentation Only
Writing the spec after the code, or keeping it in a Wiki/Notion — it will never be accurate or enforced.

### Skipping Validation in CI
If `schemathesis` or `oasdiff` does not run in CI, the contract is aspirational, not enforced.

### Shared Request/Response DTOs Between Services
Sharing Java/Go/TS types directly across services creates compile-time coupling. Share the spec file; generate independent types per service.

---

## Related Skills

- `api-design` — REST conventions (naming, status codes, pagination)
- `problem-details` — RFC 7807 error response implementation
- `strategic-ddd` — Published Language and Open Host Service patterns
- `hexagonal-typescript` / `hexagonal-java` — where generated types fit in the adapter layer

Related Skills

contract-testing

8
from marvinrichter/clarc

Contract Testing: Consumer-Driven Contract Testing with Pact (REST, messaging, Pact Broker, can-i-deploy), OpenAPI contract testing with Prism mock server and dredd, schema compatibility (ajv, Protobuf wire rules, Avro + Schema Registry), and breaking change detection with oasdiff in CI.

api-contract-advanced

8
from marvinrichter/clarc

Advanced API contract patterns — AsyncAPI 3.0 for event-driven systems and Consumer-Driven Contract Testing with Pact. Use after the core Contract-First workflow in api-contract.

zero-trust-patterns

8
from marvinrichter/clarc

Zero-Trust security patterns — mTLS between microservices (Istio/SPIFFE), SPIRE workload identity, OPA/Envoy authorization, NetworkPolicy default-deny-all, short-lived credentials, service mesh security, and Kubernetes RBAC hardening.

wireframing

8
from marvinrichter/clarc

Wireframing and prototyping workflow: fidelity levels (lo-fi sketch → mid-fi wireframe → hi-fi prototype), tool selection (Figma, Excalidraw, Balsamiq), user flow diagrams, wireframe annotation standards, information architecture (IA) mapping, and the handoff from wireframe to visual design. For developers who need to communicate UI structure before writing code.

webrtc-patterns

8
from marvinrichter/clarc

WebRTC patterns — peer connection setup, ICE/STUN/TURN configuration, signaling server design, SFU vs mesh topology, screen sharing, media track management, and reconnect/ICE restart handling.

webhook-patterns

8
from marvinrichter/clarc

Webhook patterns for receiving, verifying (HMAC), and idempotently processing third-party events. Covers Stripe, GitHub, and generic webhook patterns, delivery guarantees, retry handling, and testing.

web-performance

8
from marvinrichter/clarc

Web performance optimization: Core Web Vitals (LCP, CLS, INP), Lighthouse CI with budget configuration, bundle analysis (webpack-bundle-analyzer, vite-bundle-visualizer), hydration performance, network waterfall reading, image optimization (WebP/AVIF, srcset), and font performance.

wasm-performance

8
from marvinrichter/clarc

WebAssembly performance: wasm-opt binary optimization, size reduction (panic=abort, LTO, strip), profiling WASM in Chrome DevTools, memory management (linear memory, avoiding GC pressure), SIMD, and multi-threading with SharedArrayBuffer.

wasm-patterns

8
from marvinrichter/clarc

WebAssembly patterns: wasm-pack, wasm-bindgen (JS↔Wasm interop), WASI, Component Model, wasm-opt, Rust-to-WASM compilation, JS integration (web workers, streaming instantiation), and production deployment (CDN, Content-Type headers).

visual-testing

8
from marvinrichter/clarc

Visual Regression Testing: tool comparison (Chromatic/Percy/Playwright screenshots/BackstopJS), pixel-diff vs AI-based comparison, baseline management, flakiness strategies (masks, tolerances, waitForLoadState), CI integration with GitHub Actions, and Storybook integration.

visual-identity

8
from marvinrichter/clarc

Brand identity development: color palette construction (primary/secondary/semantic/neutral), logo concept brief writing, typeface pairings, brand voice definition, mood board direction, and Brand Guidelines document structure. Use when establishing or evolving a visual brand — not for implementing existing tokens.

ux-micro-patterns

8
from marvinrichter/clarc

UX micro-patterns for every product state: Empty States, Loading States (skeleton screens, spinners, optimistic UI), Error States, Success States, Confirmation Dialogs, Onboarding Flows, and Progressive Disclosure. These patterns apply to every feature — done wrong, they're the biggest source of user confusion.