Chapter 252-3 hours

OpenAPI Specification & Documentation

An API without documentation is a rumor, not an interface. OpenAPI has become the industry-standard contract language for APIs, turning prose agreements into machine-readable specifications that drive documentation, testing, and code generation. This chapter explains what OpenAPI is, why it exists, how to write a specification, the trade-offs between design-first and code-first workflows, and how to keep the spec and the implementation in sync over time.

Part II / The Contract Layer

The Problem OpenAPI Solves

Every API starts as an idea between two engineers. Alice builds a server. Bob writes a client. At first the contract lives in Alice’s head, or in a Slack thread, or in a handwritten markdown file that lives next to the code. It works — until it doesn’t.

The failure mode is universal and deeply human: drift. Alice adds a field to a response. Bob’s client breaks. Alice says “that’s not a breaking change,” Bob says “my code expects it.” A markdown file was never a contract — it was a conversation, and conversations drift. The cost is not the bug itself; it’s the time spent arguing about what the API actually does, time that could have been spent building.

Alicewrites codeProse speclives in markdownBobreads prosedriftnot a contract — a conversation
The drift problem

OpenAPI (formerly Swagger) solves this by making the API contract a machine-readable file — a single YAML or JSON document that describes every endpoint, every request shape, every response, every status code, and every security requirement. The spec becomes the source of truth. Documentation is generated from it. Tests are written against it. Client SDKs are generated from it. When Alice changes the spec, the tooling tells Bob immediately, not at 2 AM after a production incident.

The core insight is that a contract that machines can read is a contract that machines can enforce. You can write a CI check that rejects a pull request if the implementation doesn’t match the spec. That’s not a dream — it’s the standard workflow in teams that take API design seriously.

What OpenAPI Is (and Why It Exists)

OpenAPI Specification (OAS) is a standard, language-agnostic interface description for REST-like APIs. It defines a canonical way to describe:

  • What endpoints exist and what HTTP methods they support
  • What inputs they accept (path parameters, query parameters, request bodies, headers)
  • What outputs they produce (response bodies, headers, status codes)
  • How to authenticate (API keys, OAuth2, Bearer tokens)
  • What the data shapes look like (schemas for requests, responses, and shared types)

The spec was originally created by Tony Tam at Wordnik around 2010 as the Swagger specification, driven by the practical need to auto-generate API documentation from code. In 2015, the OpenAPI Initiative (under the Linux Foundation) took ownership, renamed it, and standardized it. OpenAPI 3.2.0 is the latest published version; the main YAML example in this chapter uses OAS 3.1.0, which remains widely used across the API tooling ecosystem — from Swagger UI to Postman to code generators in every major language.

The reason OpenAPI took off is that it sits at the intersection of three problems that every API team eventually hits: documentation that goes stale, integration tests that are actually manual, and client SDKs that are copy-pasted instead of generated. OpenAPI solves all three at once by making the spec the single source of truth from which documentation, tests, and clients are all derived.

The Anatomy of an OpenAPI Specification

An OpenAPI specification is a single YAML (or JSON) file. At its heart are these top-level keys:

openapi: 3.1.0
info:
  title: Bookstore API
  description: A simple API for managing a book collection
  version: 1.0.0
  contact:
    name: API Support
    email: support@bookstore.example.com
servers:
  - url: https://api.bookstore.example.com/v1
    description: Production server
  - url: http://localhost:3000/v1
    description: Local development server
paths:
  /books:
    get:
      summary: List all books
      ...
  /books/{bookId}:
    get:
      summary: Get a single book
      ...
components:
  schemas:
    Book:
      type: object
      ...

Each key has a specific job:

KeyPurposeRequired
openapiVersion of the OAS spec being used (e.g. 3.1.0)Yes
infoMetadata — title, description, version, contact infoYes
serversBase URLs for different environments (prod, staging, local)No
pathsEvery endpoint and its HTTP methods, parameters, and responsesYes
componentsReusable definitions — schemas, security schemes, parametersNo
tagsLogical grouping of endpoints (e.g. “Books”, “Users”)No
securityGlobal authentication requirementsNo

The info block — why metadata matters

The info object is deceptively simple. It contains the API title, description, version, and contact information. But it serves a deeper purpose than branding: the version field is how you track which spec goes with which deployed API. When you change the spec, you bump the version, and every tool that consumes the spec knows exactly what it’s looking at.

info:
  title: Bookstore API
  version: 2.1.0
  description: >
    Version 2.1 adds pagination to all list endpoints
    and introduces the `publisher` field on books.

The description supports Markdown — so you can write detailed changelogs, migration guides, and usage notes directly in the spec.

The servers block — environment awareness

Without servers, every example in the spec points to a single URL and you have to mentally adjust for staging, production, and local development. The servers array solves this by listing all environments:

servers:
  - url: https://api.example.com/v1
    description: Production
  - url: https://staging.api.example.com/v1
    description: Staging
  - url: http://localhost:4000/v1
    description: Local development

Swagger UI and other tools let you switch between servers, so the interactive documentation reflects the right base URL for the environment you’re testing against.

The paths object — every endpoint, every method

The paths object is the core of the spec. Each key is a URL path pattern. Each value is an object mapping HTTP methods to operation objects:

paths:
  /books:
    get:
      operationId: listBooks
      summary: List all books
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 10
      responses:
        "200":
          description: A paginated list of books
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Book'
                  total:
                    type: integer

Notice the pattern: every operation has an operationId (a unique identifier used by code generators), a summary, parameters, and a responses object. The responses are keyed by HTTP status code — "200", "404", "500" — and each response describes its content type and schema.

openapi: “3.1.0”The spec version identifierinfotitle, version, contactpathsevery endpoint & method/books, /books/:idGET, POST, PUT, DELETEcomponentsservers, tags, security
An OpenAPI spec at a glance

Defining Endpoints, Schemas, and Status Codes

Parameters and request bodies

OpenAPI distinguishes between parameters that live in the URL and bodies that live in the request payload:

parameters:
  - name: bookId
    in: path          # URL path parameter — always required
    required: true
    schema:
      type: string
      pattern: '^[a-zA-Z0-9_-]+$'
  - name: filter
    in: query         # Query parameter — optional
    required: false
    schema:
      type: string
      enum: [active, archived, all]
      default: all
  - name: X-Request-ID
    in: header        # Header parameter
    required: false
    schema:
      type: string
      format: uuid
requestBody:          # Body parameter — for POST/PUT/PATCH
  required: true
  content:
    application/json:
      schema:
        $ref: '#/components/schemas/CreateBook'

The in field determines where the parameter lives: path (part of the URL), query (after ?), header (in the HTTP headers), or cookie. Path parameters are always required; query, header, and cookie parameters can be optional. The requestBody is separate and only applies to methods that carry a payload.

Response schemas and status codes

Every response must have a status code key and a description. The content block specifies the media type and schema:

responses:
  "200":
    description: Successfully retrieved a book
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Book'
  "404":
    description: Book not found
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Error'
  "500":
    description: Internal server error

Status codes in OpenAPI follow the same semantics as HTTP: 2xx for success, 4xx for client errors, 5xx for server errors. A well-specified API documents at least the success case and the most important error cases — the ones a consumer actually needs to handle.

Reusable schemas with components

The components/schemas section is where you define reusable data types. Every parameter and response can reference them with $ref:

components:
  schemas:
    Book:
      type: object
      required: [id, title, author]
      properties:
        id:
          type: integer
          readOnly: true
        title:
          type: string
          minLength: 1
          maxLength: 200
        author:
          type: string
          minLength: 1
        isbn:
          type: [string, "null"]
          format: isbn
        publishedAt:
          type: string
          format: date-time
      example:
        id: 42
        title: "Designing Data-Intensive Applications"
        author: "Martin Kleppmann"
    Error:
      type: object
      required: [code, message]
      properties:
        code:
          type: integer
        message:
          type: string
        details:
          type: object
    CreateBook:
      type: object
      required: [title, author]
      properties:
        title:
          type: string
          minLength: 1
        author:
          type: string
        isbn:
          type: string
          format: isbn

Notice the readOnly: true on id — this tells consumers that id is always present in responses but must never be sent in requests. The example field provides a concrete sample that Swagger UI renders in the interactive documentation.

Authentication and Security Schemes

OpenAPI defines security at two levels: globally (applying to all endpoints by default) and per-operation (overriding or adding specific requirements).

Defining security schemes

The components/securitySchemes object declares every authentication mechanism the API supports:

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    OAuth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://auth.example.com/oauth/authorize
          tokenUrl: https://auth.example.com/oauth/token
          scopes:
            read: Read access
            write: Write access
            admin: Full administrative access

Three common scheme types cover most real-world APIs:

TypeWhere the credential goesTypical use
apiKeyHeader, query, or cookieSimple key-based auth (e.g. X-API-Key)
httpAuthorization headerBearer tokens, Basic Auth
oauth2OAuth2 flowsThird-party delegated access

Applying security globally and per-operation

Apply security globally so every endpoint requires authentication unless explicitly overridden:

security:
  - BearerAuth: []          # All endpoints require Bearer token by default

paths:
  /books:
    get:
      security:             # Override: this endpoint is public
        []
      ...
  /books/{id}:
    get:
      security:
        - OAuth2: [read]      # Requires the "read" scope
      ...
  /books:
    post:
      security:
        - OAuth2: [write]     # Requires the "write" scope

The empty array [] as the security value means “this endpoint requires no authentication” — a deliberate override of the global default. The array of scopes on a security scheme restricts which permissions the caller must hold.

apiKeyHeader, query, or cookieX-API-Key: abc123httpAuthorization headerBearer <JWT>oauth2OAuth2 flowsauth/token URLsCredential location determined by the scheme type
Security scheme types and where credentials live

Swagger UI and Interactive Documentation

The single biggest practical benefit of an OpenAPI spec is that it can be rendered into interactive documentation by tools like Swagger UI, Redoc, or Scalar. Swagger UI parses the spec and produces a self-documenting website where users can:

  1. Browse every endpoint organized by tag
  2. Expand any operation to see parameters, request body, and responses
  3. Try it out — fill in parameter values, hit “Execute”, and see the real response
  4. See example values — from example fields in the spec
  5. Understand auth — see which endpoints require which security schemes
openapi.yamlpaths, schemas, securitymachine-readable specSwagger UIInteractive docsTry endpoints, see examplesGenerated from specOne spec, many renderers
From spec to interactive documentation

Serving with Swagger UI

The simplest way to get interactive docs is to serve the spec through Swagger UI. This can be done via npm packages, Docker, or hosted services like Scalar or Redoc. For a simple static setup:

<!-- Minimal Swagger UI HTML -->
<!DOCTYPE html>
<html>
<head>
  <title>API Docs</title>
  <script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">
</head>
<body>
  <div id="swagger"></div>
  <script>
    SwaggerUIBundle({
      url: "/openapi.yaml",
      dom_id: "#swagger",
      presets: [SwaggerUIBundle.Presets.APIs],
    });
  </script>
</body>
</html>

The url points to your OpenAPI spec file. Swagger UI fetches it, parses it, and renders the full interactive documentation. Every change to the spec immediately updates the docs — no manual maintenance.

Other renderers

  • Redoc — More polished, supports one-page layout and deep linking to definitions
  • Scalar — Modern, FastAPI-native, excellent dark mode
  • Stoplight Elements — Embeddable widget if you don’t want a full docs page

All of them consume the same OpenAPI spec, so you write the spec once and choose your renderer based on branding needs.

Design-First vs Code-First

There are two fundamentally different philosophies for creating an OpenAPI spec, and the choice affects how you structure your entire API workflow.

Design-first

In the design-first approach, you write the OpenAPI spec before writing any server code. The spec is the contract, the code implements it, and everything is validated against it.

The workflow:

  1. Define the API in openapi.yaml — endpoints, schemas, status codes, security
  2. Validate the spec with a linter (e.g. openapi-lint)
  3. Generate server stubs and client SDKs from the spec
  4. Implement the server by filling in the generated handler logic
  5. Run tests that verify the implementation matches the spec

Advantages:

  • The contract is agreed upon before implementation starts — frontend and backend teams can work in parallel
  • The spec is a proper design artifact, reviewed and versioned independently of code
  • Client SDKs are generated at the same time as the server, so they’re always available
  • Easier to maintain consistency across multiple services because they all share the same spec

Disadvantages:

  • The spec must be kept in sync with the code manually (more on this below)
  • Refactoring the implementation means editing both the spec and the code
  • Can feel heavy for small, rapidly evolving APIs

Code-first

In the code-first approach, you write the server code first and generate the OpenAPI spec from it. The code defines the API, and the spec is a derived artifact.

The workflow:

  1. Write server code with route decorators and Pydantic/FastAPI models
  2. Generate the OpenAPI spec at build time or runtime
  3. Serve the spec through Swagger UI
  4. Generate client SDKs from the generated spec
  5. Write tests against the generated spec

Advantages:

  • The spec is always in sync with the code — it’s generated, not hand-written
  • Faster iteration for small teams — write code, the spec appears automatically
  • Less context switching between spec files and implementation files
  • Frameworks like FastAPI make this trivial

Disadvantages:

  • The spec inherits the constraints of the code — you can’t easily express design decisions that the code doesn’t support
  • Harder to get cross-team agreement on the API shape before implementation
  • The spec may contain implementation details that don’t belong in a public contract

When to choose which

FactorDesign-firstCode-first
Team size3+ engineers, multiple teamsSolo or small team
API stabilityStable contract, long-lived APIEvolving rapidly
External consumersYes (partner APIs)Internal only
Frontend teamSeparate frontend teamFull-stack developer
MaturityEstablished API, formal processPrototype or new service

Most mature API teams start with design-first and migrate toward code-first as the team and API stabilize. Some teams use a hybrid: design the initial contract (design-first), generate stubs, implement (code-first), and then keep the spec in sync via CI validation.

Code Generation and Type-Safe API Clients

An OpenAPI spec is not just documentation — it’s a source of truth that machines can consume. Code generators read the spec and produce ready-to-use client SDKs, server stubs, and TypeScript types for every language in the ecosystem.

ToolOutputLanguageNotes
openapi-generatorClient SDKs, server stubs, docs40+ languagesThe most comprehensive generator
orvalTypeScript clients with fetchersTypeScriptWorks with OpenAPI 3.x, excellent DX
swagger-typescript-apiTypeScript types and API clientTypeScriptLightweight, fast
openapi-typescriptTypeScript types onlyTypeScriptMinimal, zero-runtime
mockttpMock serversFrom the spec

Generating a TypeScript client

With orval, generating a type-safe TypeScript client from a spec is a single command:

npx orval --input ./openapi.yaml --output ./src/api/client

This produces:

  • Typed function calls for every endpoint (getBooks(), createBook(data), getBookById(id))
  • TypeScript types for every schema (Book, CreateBook, Error)
  • Request/response type inference — the compiler knows what shape each call returns
// Auto-generated from the spec
export const getBooks = (params?: { page?: number; limit?: number }) =>
  fetcher<{ data: Book[]; total: number }>('/books', { method: 'GET', params });

export type Book = {
  id: number;
  title: string;
  author: string;
  isbn?: string;
  publishedAt?: string;
};

The result: your frontend code gets compile-time type checking for every API call. If the spec changes, the generated types change, and TypeScript tells you exactly which files need updating. No more any and no more guessing at runtime.

Generating server stubs

openapi-generator can also generate server stubs — skeleton controller files in Go, Python, Java, or any supported language — that you fill in with business logic:

npx @openapi-generator/cli generate \
  -i openapi.yaml \
  -g go \
  -o ./server/generated

This produces typed handlers with the correct method signatures, parameter bindings, and response types. You implement the handlers by filling in the generated function bodies.

Keeping the Spec in Sync

The hardest part of OpenAPI is not writing the spec — it’s keeping it in sync with the implementation as the API evolves. A stale spec is worse than no spec, because it actively misleads consumers.

Validation in CI

Add a validation step to your CI pipeline that checks the spec against the running API:

# .github/workflows/openapi-validation.yml
name: Validate OpenAPI Spec
on: [pull_request]
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm install
      - run: npm run build
      - run: npm run start &      # Start the upstream application
      - run: npx prism proxy openapi.yaml http://localhost:4000 --port 4010 &
      - run: curl --fail http://localhost:4010/books # Send requests through Prism

Prism’s validation-proxy mode requires the application to be running first. It validates requests and responses routed through the proxy; it does not start the upstream application or generate a request for every operation. Start the application separately, start Prism against it, and send your tests through the Prism proxy. For spec-driven request generation and response validation, use dredd instead.

Linting the spec

Lint the spec itself for common problems before it ever reaches production:

  • openapi-lint — Checks for missing descriptions, invalid examples, missing status codes
  • spectral — Enforces custom rules (e.g., “all endpoints must have a 4xx response”, “all schemas must have an example”)
  • speccy — Validates syntax and checks for common mistakes
- run: npx spectral lint openapi.yaml

Drift detection

For code-first teams, detect drift between the generated spec and the hand-written spec at build time:

# Generate the spec from code
npm run build:spec       # Produces generated-openapi.yaml

# Compare with the canonical spec
npx json-diff openapi.yaml generated-openapi.yaml

If there are differences, either the hand-written spec is outdated (update it) or the code has drifted from the contract (fix the code). Either way, the discrepancy surfaces automatically.

Versioning the spec

Treat the OpenAPI spec like any other source artifact:

  • Version it with the API version (v1/openapi.yaml, v2/openapi.yaml)
  • Store it in Git alongside the code — never in a separate wiki or Confluence
  • Automate updates with a CI pipeline that regenerates the spec and opens a PR if the hand-written version doesn’t match
  • Add a changelog in the spec’s description field, using Markdown
Write / Update SpecYAML, machine-readableValidate with CIGenerate Client SDKsTypeScript, Go, PythonType-safe, always currentServe DocsSwagger UI / ScalarInteractive, liveCI ValidationSpec matches server
The spec maintenance loop

OpenAPI for AI Agents

An AI agent is another kind of API client: it chooses an operation, supplies arguments, and interprets the response. OpenAPI gives the agent runtime a machine-readable description of those operations, but it does not turn an API into an autonomous system by itself. An adapter converts selected OpenAPI operations into the tool format understood by the model, while the application remains responsible for authorization, execution, and side effects.

For example, an adapter can turn this operation:

paths:
  /books/{bookId}:
    get:
      operationId: getBook
      summary: Get a book by ID
      parameters:
        - name: bookId
          in: path
          required: true
          schema:
            type: integer

into a tool with a stable name, a useful description, and a validated argument schema:

{
  "name": "getBook",
  "description": "Get a book by its numeric ID.",
  "parameters": {
    "type": "object",
    "required": ["bookId"],
    "properties": {
      "bookId": { "type": "integer" }
    },
    "additionalProperties": false
  }
}

The model may propose a tool call, but the agent runtime should validate it against the OpenAPI-derived schema before making an HTTP request. It should also resolve the path and method from a trusted operation registry rather than allowing the model to construct arbitrary URLs. operationId, summaries, descriptions, enums, formats, and examples therefore matter twice: they improve human documentation and give the model clearer boundaries for choosing and calling a tool.

Production boundaries for agentic API calls

Treat an agent tool call like an untrusted request crossing a public API boundary:

  • Expose an allowlist. Publish only the operations the agent needs. Do not expose administrative or destructive endpoints merely because they appear in the full OpenAPI document.
  • Authorize the caller. Enforce the end user’s identity, tenant, permissions, and scopes on the server for every call. A model’s decision is never proof of authorization.
  • Separate reads from writes. Read operations can often run automatically; create, update, delete, payment, and permission-changing operations should require explicit confirmation or a policy decision.
  • Validate twice. Validate arguments before dispatch, then validate the upstream response before returning it to the model. Reject unexpected fields, status codes, and content types rather than silently guessing.
  • Control execution. Apply timeouts, rate limits, retry budgets, idempotency keys, and maximum response sizes. Tool loops need a step limit and a cancellation path.
  • Trace the decision. Log the user, model request, selected operationId, sanitized arguments, authorization result, latency, status code, and request ID. Keep secrets and sensitive response data out of ordinary logs.

Keep the OpenAPI document versioned and pin the agent adapter to a known version. A renamed operation, broadened schema, or newly exposed write endpoint can change agent behavior even when no prompt changes. Validate generated tool definitions in CI, exercise representative tool calls against a mock or staging service, and review permission changes like any other API contract change.

The chapter in one breath

OpenAPI is not a documentation tool — it’s a contract language for APIs. Write the spec, validate it, generate from it, and keep it in sync. The spec becomes the single source of truth that drives documentation, testing, client SDKs, and CI checks. Teams that adopt this workflow stop arguing about what their API does and start building on top of it.