API · Openapi

OpenAPI Spec Generation — A Field Guide

Code-first vs spec-first OpenAPI generation: which tools fit which ecosystem, how to keep the spec from drifting out of sync with the implementation, and where contract testing fits in.

John Kihiu12 min read

There are two fundamentally different ways an OpenAPI spec gets into existence: written by hand (or generated from code annotations) after the API already exists, or designed first and used to generate the implementation, mocks, and docs before a line of business logic is written. Teams tend to pick one without examining the tradeoff, and the choice quietly shapes how much the spec can be trusted later.

Code-first: annotations generating the spec

Code-first means you write the API in your framework of choice and derive the OpenAPI spec from annotations, decorators, or type signatures on the actual route handlers. In the Node ecosystem, @nestjs/swagger reads decorators on NestJS controllers and DTOs; in Python, FastAPI generates a fully accurate OpenAPI spec automatically from your Pydantic models and route type hints with essentially zero extra annotation; in Go, `swaggo/swag` parses specially formatted comments above handler functions. The appeal is that the spec can never drift too far from reality, since it's derived directly from the code that actually runs.

PYTHON · CODE-FIRST WITH FASTAPI
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Invoice(BaseModel):
    id: str
    amount: float
    status: str

@app.get("/invoices/{invoice_id}", response_model=Invoice)
async def get_invoice(invoice_id: str) -> Invoice:
    return await invoice_service.find(invoice_id)

# GET /openapi.json now reflects this route and the Invoice
# schema automatically — no separate spec file to maintain

Spec-first: designing the contract before the code

Spec-first flips the order: you author the OpenAPI YAML/JSON directly (often collaboratively, in a tool like Stoplight or just hand-written in an editor with schema validation), then generate server stubs, client SDKs, and mock servers from that spec before the real implementation exists. This lets frontend and backend teams work in parallel against a mocked API — Prism or a similar tool can serve fake responses straight from the spec — and forces an explicit design conversation about the contract before anyone commits to an implementation shape.

YAML · SPEC-FIRST CONTRACT
paths:
  /invoices/{invoiceId}:
    get:
      operationId: getInvoice
      parameters:
        - name: invoiceId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Invoice'
        '404':
          description: Invoice not found

Run npx @stoplight/prism-cli mock openapi.yaml against that file and frontend developers get a working mock API immediately, before the backend team writes a single handler.

Spec-first forces the hard conversations earlier

Deciding error response shapes, pagination style, and field naming conventions in a shared spec document, before implementation, surfaces disagreements when they're cheap to resolve. The same disagreements discovered after code-first implementation usually mean a breaking change to a live API.

Keeping the spec in sync with the implementation

Code-first mostly solves drift by construction — the spec is derived from the code, so it can't diverge unless the generation step is skipped. Spec-first has the opposite risk: nothing stops an implementer from adding an undocumented field or changing a response shape without updating the spec, and the spec silently becomes fiction. The fix, regardless of which approach you started with, is automated contract testing that fails the build the moment implementation and spec disagree — not a documentation review that happens occasionally and gets skipped under deadline pressure.

An unenforced spec is a comment, not a contract

A spec-first OpenAPI file with no automated check against the running service degrades exactly like a stale code comment — accurate on day one, quietly wrong by month six. If nothing fails CI when they diverge, don't call it "the contract"; call it documentation, and treat it with the same skepticism.

Where contract testing fits in

Tools like Dredd or Schemathesis take an OpenAPI spec and run the real API against it, asserting that actual responses match the documented schema — status codes, field types, required fields all checked against the spec rather than a hand-written test case. This closes the loop for code-first (verifying the generation didn't silently fall out of sync with runtime behavior in some edge case) and for spec-first (verifying the implementation actually matches what was designed). Either way, contract tests running in CI are what turn "the spec is documentation" into "the spec is enforced," which is the only version of either workflow worth trusting long-term.

Wrapping up

Code-first keeps the spec honest by deriving it from what's actually running; spec-first lets you design the contract and parallelize frontend and backend work before either exists. Neither approach is inherently better, but neither is safe without contract tests enforcing the spec against the real API in CI — pick the workflow that fits your team, then don't skip the enforcement step.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.