Lesson 23 / 27

Contract-First with OpenAPI

Describe the API in an OpenAPI document and lint and diff it automatically.

One document, many uses

An OpenAPI document (YAML or JSON) describes paths, parameters, request and response schemas, errors and security in a machine-readable form. Designing it first (contract-first) lets teams review and mock the API before implementation, and the same file then drives interactive docs, client SDK and server stub generation, request/response validation, contract tests and breaking-change checks between versions. Reuse definitions with $ref under components, give every operation a unique operationId, and describe error responses too. Keep the spec in version control next to the code and fail the build if the implementation drifts from it.

Keep the promise in code

A machine-readable contract, automated checks and shared standards keep many teams consistent.

Three layers: spec, tests, standards.
Figure 7.1 — Spec, tests and standards.

A small OpenAPI 3.1 document

Describes the v2 list and get endpoints with a reusable Item schema, a problem+json error and a rate-limit response. I parsed it with PyYAML in the next snippet.

openapi: 3.1.0
info:
  title: Items API
  version: "2.0.0"
servers:
  - url: https://api.example.com
paths:
  /v2/items:
    get:
      operationId: listItems
      summary: List items (cursor pagination)
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
        - name: cursor
          in: query
          schema: { type: string }
      responses:
        "200":
          description: A page of items
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ItemPage" }
        "429":
          $ref: "#/components/responses/RateLimited"
  /v2/items/{id}:
    get:
      operationId: getItem
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: One item
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Item" }
        "404":
          description: Not found
          content:
            application/problem+json:
              schema: { $ref: "#/components/schemas/Problem" }
components:
  schemas:
    Item:
      type: object
      required: [id, first_name, last_name]
      properties:
        id: { type: integer }
        first_name: { type: string }
        last_name: { type: string }
        plan: { type: string, enum: [free, pro] }
    ItemPage:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/Item" }
        next_cursor: { type: [string, "null"] }
    Problem:
      type: object
      required: [title, status]
      properties:
        type: { type: string }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }
  responses:
    RateLimited:
      description: Too many requests
      headers:
        Retry-After: { schema: { type: integer } }

Checking the spec structure, run

I ran this plain-Python example. Save the YAML above as openapi.yaml; this script loads it, lists the operations, checks operationId uniqueness and confirms all 5 $ref pointers resolve inside the document. It is a teaching check; real projects use dedicated validators and linters.

import yaml
spec = yaml.safe_load(open("openapi.yaml"))
ops, refs = [], []
def walk(o):
    if isinstance(o, dict):
        if "$ref" in o: refs.append(o["$ref"])
        for v in o.values(): walk(v)
    elif isinstance(o, list):
        for v in o: walk(v)
walk(spec)
for path, item in spec["paths"].items():
    for method, op in item.items(): ops.append((method.upper(), path, op["operationId"]))
def resolve(ref):
    node = spec
    for part in ref.lstrip("#/").split("/"): node = node[part]
    return node
missing = [r for r in refs if not _ok(r)] if False else []
for r in refs:
    try: resolve(r)
    except KeyError: missing.append(r)
print(spec["openapi"], len(spec["paths"]), "paths")
print(ops)
print("unique operationIds:", len({o[2] for o in ops}) == len(ops), "| refs:", len(refs), "| unresolved:", missing)

Output:

3.1.0 2 paths
[('GET', '/v2/items', 'listItems'), ('GET', '/v2/items/{id}', 'getItem')]
unique operationIds: True | refs: 5 | unresolved: []

Quick check: What is a benefit of designing the OpenAPI contract first?

  • Teams can review, mock and generate code before implementing
  • It removes the need for any code
  • It makes APIs slower
  • It prevents all bugs
Answer

Teams can review, mock and generate code before implementing — A shared, reviewable contract aligns producers and consumers early.