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.
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:
| Key | Purpose | Required |
|---|---|---|
openapi | Version of the OAS spec being used (e.g. 3.1.0) | Yes |
info | Metadata — title, description, version, contact info | Yes |
servers | Base URLs for different environments (prod, staging, local) | No |
paths | Every endpoint and its HTTP methods, parameters, and responses | Yes |
components | Reusable definitions — schemas, security schemes, parameters | No |
tags | Logical grouping of endpoints (e.g. “Books”, “Users”) | No |
security | Global authentication requirements | No |
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.
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:
| Type | Where the credential goes | Typical use |
|---|---|---|
apiKey | Header, query, or cookie | Simple key-based auth (e.g. X-API-Key) |
http | Authorization header | Bearer tokens, Basic Auth |
oauth2 | OAuth2 flows | Third-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.
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:
- Browse every endpoint organized by tag
- Expand any operation to see parameters, request body, and responses
- Try it out — fill in parameter values, hit “Execute”, and see the real response
- See example values — from
examplefields in the spec - Understand auth — see which endpoints require which security schemes
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:
- Define the API in
openapi.yaml— endpoints, schemas, status codes, security - Validate the spec with a linter (e.g.
openapi-lint) - Generate server stubs and client SDKs from the spec
- Implement the server by filling in the generated handler logic
- 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:
- Write server code with route decorators and Pydantic/FastAPI models
- Generate the OpenAPI spec at build time or runtime
- Serve the spec through Swagger UI
- Generate client SDKs from the generated spec
- 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
| Factor | Design-first | Code-first |
|---|---|---|
| Team size | 3+ engineers, multiple teams | Solo or small team |
| API stability | Stable contract, long-lived API | Evolving rapidly |
| External consumers | Yes (partner APIs) | Internal only |
| Frontend team | Separate frontend team | Full-stack developer |
| Maturity | Established API, formal process | Prototype 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.
Popular generators
| Tool | Output | Language | Notes |
|---|---|---|---|
| openapi-generator | Client SDKs, server stubs, docs | 40+ languages | The most comprehensive generator |
| orval | TypeScript clients with fetchers | TypeScript | Works with OpenAPI 3.x, excellent DX |
| swagger-typescript-api | TypeScript types and API client | TypeScript | Lightweight, fast |
| openapi-typescript | TypeScript types only | TypeScript | Minimal, zero-runtime |
| mockttp | Mock servers | — | From 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 codesspectral— 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
descriptionfield, using Markdown
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.