Module 09: OpenAPI Standards
Why this matters
Every design decision from module 08 — resource URLs, status codes, request/response schemas, versioning — is a contract between your API and its clients. Right now that contract lives in your head and, implicitly, in your code. OpenAPI is what makes it explicit, machine-readable, and shared. An OpenAPI document is a standardized description of your entire API: every path, every method, every parameter, every request and response schema, every status code, every security requirement — written in a format (JSON or YAML) that both humans and tools understand. Once that document exists, an enormous amount becomes automatic: interactive documentation, client SDK generation in a dozen languages, request mocking, contract testing, and API gateways that can validate traffic against the spec.
Here's the part that makes this module easy to underestimate and then love:
FastAPI generates the OpenAPI document for you, from the code you've already
written. Every Pydantic model, every Query(...) constraint, every
response_model, every status code you declared across modules 00–08 has been
quietly feeding a spec this whole time. Visit /docs and there's a complete,
interactive Swagger UI you didn't write a line of documentation for. That's
not a gimmick — it's the direct payoff of being precise with your types and
declarations. Sloppy code produces a sloppy spec; the discipline this track
taught produces a spec that's genuinely useful as a client contract.
The deeper idea is API-first (or spec-first) development: write the OpenAPI contract before implementing, agree on it with the client teams, and then implement to satisfy it. This flips the usual order — instead of the code accidentally defining the contract, the contract deliberately defines the code. Even when you generate the spec from code (as FastAPI does), thinking spec-first is what keeps an API coherent as it grows across many endpoints and many engineers.
Concepts
What OpenAPI is (and the Swagger history)
OpenAPI is a specification — a formal, language-agnostic standard for
describing HTTP APIs. A concrete API description written to that standard is
an OpenAPI document (often one openapi.json or openapi.yaml file). Some
history that clears up the confusing name soup: the project began as
Swagger, created around 2011. In 2015 the specification was donated to the
Linux Foundation and renamed the OpenAPI Specification (OAS); "Swagger"
now refers to the tooling built around it (Swagger UI, Swagger Editor,
Swagger Codegen), while "OpenAPI" is the specification itself. So: OpenAPI =
the standard; Swagger = a family of tools. People still say "Swagger docs"
loosely to mean the interactive UI.
Why a machine-readable contract is powerful
Because the document is structured data, tools can consume it:
- Interactive documentation (Swagger UI, Redoc) — a live, browsable, try-it-out UI generated from the spec. No hand-written, drift-prone docs.
- Client SDK generation — generate a typed client library in Python, TypeScript, Go, Java, etc., directly from the spec, so clients don't hand-code HTTP calls.
- Server stubs / mocking — generate a mock server that returns spec-conformant fake responses, so a frontend team can build against the API before the backend exists.
- Contract testing & validation — assert that your running API still matches the agreed spec; API gateways can reject requests that violate it.
- Postman/Insomnia import — load the spec into an API client and get every endpoint preconfigured.
The single artifact drives the whole ecosystem. That's why precision pays off.
The structure of an OpenAPI document
An OpenAPI 3.x document has a handful of top-level sections:
openapi: 3.1.0 # spec version
info: # metadata: title, version, description
title: Task API
version: 1.0.0
servers: # base URLs the API is served from
- url: https://api.example.com/v1
paths: # THE endpoints: /tasks, /tasks/{id}, ...
/tasks:
get:
summary: List tasks
parameters: [...] # query/path/header params
responses: # status code -> response schema
"200": {...}
post:
requestBody: {...} # the input schema
responses:
"201": {...}
components: # reusable pieces
schemas: # your Pydantic models land here
Task: {...}
securitySchemes: # how auth works (bearer, apiKey, oauth2)
bearerAuth: {...}
security: # which schemes apply globally
- bearerAuth: []
info— metadata (title, version, description). The version here is your API's version, distinct from the OpenAPI spec version.paths— the heart: every URL, its methods, parameters, request bodies, and per-status-code responses.components— reusable definitions, especiallyschemas(your data models, referenced with$refso they're defined once) andsecuritySchemes.security— which security schemes are required, globally or per operation.responses— the possible responses per operation, keyed by status code, each with a schema and description.
OpenAPI 3.0 vs 3.1
3.1 (2021) is the current line. The headline change: 3.1 is fully aligned
with JSON Schema (the standard Pydantic itself speaks), which removes
long-standing quirks — e.g. proper null handling via type arrays
(type: ["string", "null"]), and examples as a list. 3.0 used a slightly
divergent schema dialect and a single nullable: true flag. FastAPI emits
3.1 by default in current versions. Practically: prefer 3.1 unless a specific
downstream tool only understands 3.0, and know that Optional[str] /
str | None in Pydantic serializes cleanly to the 3.1 nullable form.
How FastAPI generates the spec — and how to enrich it
YOUR CODE GENERATED SPEC TOOLS CONSUME IT
Pydantic models ─┐ ┌──────────────┐ ┌─► /docs (Swagger UI)
Field(...) rules ─┼─FastAPI►│ openapi.json │──┼─► /redoc (reference)
response_model ─┤ builds │ (OpenAPI 3.1)│ ├─► client SDK generators
status_code ─┤ └──────────────┘ ├─► mock servers
security utils ─┘ one artifact └─► Postman / contract tests
FastAPI builds the OpenAPI document from your route declarations and Pydantic models automatically, and serves:
/openapi.json— the raw generated OpenAPI document./docs— Swagger UI (interactive, try-it-out)./redoc— Redoc (clean, reference-style documentation).
Everything you declared feeds it: path/query params become parameters,
Field(...) constraints become schema validations, response_model becomes
the response schema, status_code and additional responses= become the
documented statuses. You enrich it with metadata:
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
app = FastAPI(
title="Task API",
version="1.0.0",
description="A task management API for the api-layer track.",
)
class TaskOut(BaseModel):
id: int = Field(..., description="Unique task identifier", examples=[42])
title: str = Field(..., description="Human-readable task title")
@app.get("/tasks", tags=["tasks"], summary="List tasks",
response_model=list[TaskOut],
responses={404: {"description": "No tasks found"}})
async def list_tasks(limit: int = Query(20, ge=1, le=100, description="Page size")):
...
tags group endpoints in the UI; summary/description document them;
description/examples on Field document each property; responses=
documents non-default status codes. Docstrings on the function also become the
operation description. The lesson from earlier modules pays off directly:
because you used response_model, capped limit with le=, and defined
separate *Out models, the generated spec is accurate — it tells clients
exactly what they'll get.
Declaring security in the spec
Auth (modules 04–05) shows up in the spec via security schemes, so the
docs know an endpoint needs a token and Swagger UI shows an "Authorize"
button. FastAPI's security utilities (HTTPBearer, OAuth2PasswordBearer,
APIKeyHeader) both enforce the auth and register the scheme in OpenAPI:
from fastapi.security import HTTPBearer
from fastapi import Depends
bearer = HTTPBearer()
@app.get("/me", dependencies=[Depends(bearer)])
async def me(): ...
# -> OpenAPI now lists bearerAuth as required for /me, and /docs shows Authorize
(Track 03 implements real auth; here the point is that the scheme becomes part of the contract automatically.)
API-first / spec-first development
Two workflows:
CODE-FIRST (FastAPI default) SPEC-FIRST (API-first)
write code ─► generate spec write spec ─► review with teams
│ │ │
▼ ▼ ▼
spec = side effect implement to it mock in parallel
(can't drift, but agreed late) (agreed up front, before code exists)
- Code-first (FastAPI's default): write code, generate the spec. Fast, and the spec can't drift from the code because it is the code. Risk: the contract is a side effect, so cross-team agreement happens late.
- Spec-first / API-first: write the OpenAPI document first, review it with client teams and stakeholders, then implement to satisfy it (and/or generate server stubs and models from it). The contract is agreed before code exists, so frontend/mobile/partner teams can build against a mock in parallel, and breaking changes are caught in review, not production.
Neither is universally right. FastAPI makes code-first excellent if you're disciplined (precise types, explicit responses, consistent envelopes — this whole track). Spec-first shines for public APIs and multi-team products where the contract must be negotiated up front. Even in code-first, adopt the spec-first mindset: decide the contract deliberately, then make the code express it.
Command reference
| Item | Purpose | Example |
|---|---|---|
FastAPI(title=, version=, description=) |
Spec info metadata |
app constructor |
/openapi.json |
The generated OpenAPI document | GET /openapi.json |
/docs |
Swagger UI (interactive) | browser |
/redoc |
Redoc reference docs | browser |
tags=["..."] |
Group operations in the UI | on route or APIRouter(tags=) |
summary= / description= / docstring |
Document an operation | on the route |
response_model= |
Documented + enforced response schema | module 06/07 |
responses={404: {...}} |
Document non-default status codes | on the route |
Field(..., description=, examples=) |
Document a schema property | in a Pydantic model |
openapi_tags=[{...}] |
Describe tag groups | FastAPI(openapi_tags=...) |
| security utilities | Register auth schemes in the spec | HTTPBearer(), OAuth2PasswordBearer(...) |
The spec is only as good as your declarations. If you return a raw dict
instead of a response_model, the spec can't describe the response — clients
see an empty/opaque schema. Everything this track drilled (typed models,
explicit statuses, Field constraints) is what makes the auto-generated spec
trustworthy. Garbage in, garbage spec.
Export the spec for tooling. You can grab the document programmatically
(app.openapi() returns the dict) or fetch /openapi.json, then feed it to
openapi-generator / swagger-codegen for client SDKs, or to Redoc/Postman.
Commit a snapshot to catch accidental breaking changes in code review
(contract testing).
Customize, don't fight, the generator. Prefer enriching via tags,
summary, description, responses=, and Field metadata. Only override
app.openapi() wholesale for genuinely custom needs — the auto-generated
document is correct by construction if your declarations are.
Version metadata vs. URL versioning. info.version (module 09) documents
your API's release version; the /v1 URL prefix (module 08) is the routing
version clients call. They're related but distinct — set both deliberately.
Hands-on exercises
Continue in the api-layer project. You've already got endpoints; now make
the spec first-class.
1. Find the spec you already have
With the app running, open http://127.0.0.1:8000/docs,
http://127.0.0.1:8000/redoc, and http://127.0.0.1:8000/openapi.json.
Expected: a complete interactive UI and a JSON document describing every
endpoint you built across modules 00–08 — without writing any docs. Skim the
JSON and locate paths, components.schemas, and info.
2. Add rich info metadata
app = FastAPI(
title="API Layer Track — Task & Order API",
version="1.0.0",
description="Reference API built across the api-layer-and-request-handling track.",
contact={"name": "You", "email": "you@example.com"},
)
Reload /docs. Expected: the title, version, and description now head the
documentation page. Re-fetch /openapi.json and confirm the info block
reflects them.
3. Group endpoints with tags
Add tags=["tasks"] / tags=["orders"] to your routers (or routes). Add
descriptions for the tag groups:
app = FastAPI(openapi_tags=[
{"name": "tasks", "description": "Create, list, and manage tasks."},
{"name": "orders", "description": "Order placement and lifecycle."},
])
Reload /docs. Expected: endpoints are grouped under labeled, described
sections instead of one flat list.
4. Document schemas with descriptions and examples
Enrich a Pydantic model:
class TaskOut(BaseModel):
id: int = Field(..., description="Unique task id", examples=[42])
title: str = Field(..., description="Task title", examples=["Write the report"])
done: bool = Field(False, description="Whether the task is complete")
Reload /docs and expand the schema. Expected: each field shows its
description and example; the "Example Value" for the endpoint uses them. This
is documentation that can't drift from the code, because it's in the code.
5. Document non-default responses
@router.get("/tasks/{task_id}", response_model=TaskOut,
responses={404: {"description": "Task not found"},
304: {"description": "Not modified (ETag matched)"}})
async def get_task(task_id: int): ...
Reload /docs. Expected: the operation now lists 200, 404, and 304 with
descriptions, so clients see every outcome — matching the real behavior you
built in modules 07–08.
6. Show auth in the spec
Add HTTPBearer to a protected route (exercise from module 09 Concepts).
Reload /docs. Expected: an "Authorize" button appears, the protected
endpoint shows a padlock, and /openapi.json gains a
components.securitySchemes.HTTPBearer entry plus a security requirement on
that operation. The auth contract is now documented automatically.
7. Export the spec and generate a client (or inspect it)
curl -s localhost:8000/openapi.json -o openapi.json
# If you have it: generate a typed Python client
# npx @openapitools/openapi-generator-cli generate -i openapi.json -g python -o ./client
Even without the generator installed, open openapi.json and trace one
endpoint from paths → its requestBody/responses → the $ref into
components.schemas. Expected: you can see how a tool would turn this into a
typed client. Commit openapi.json so a future diff reveals breaking changes.
8. Diagnose and fix
This endpoint produces a useless, misleading spec: /docs shows an opaque
response schema, no status codes besides 200, and a required field the code
doesn't actually require. Fix the code so the generated spec becomes
accurate.
@router.post("/tasks") # (A) no response_model, returns raw dict
async def create_task(body: dict): # (B) untyped body -> opaque request schema
if "title" not in body:
return {"error": "title required"} # (C) 200 for an error; not documented
return {"id": 1, "title": body["title"], "secret_internal_flag": True} # (D) leaks + undocumented
Solution
Every problem is a declaration problem — fix the code and the spec fixes itself:
- (B) Untyped
body: dict. OpenAPI can't describe the request; clients see a free-form object. Declare a PydanticTaskCreatemodel so the request schema is precise (and validation is real — modules 00–02). - (A) No
response_model. The response schema is opaque. Addresponse_model=TaskOutso the spec (and clients) know the exact shape. - (C)
200for a missing field. Both wrong behavior and undocumented. With aTaskCreatemodel, a missingtitleis an automatic422(which FastAPI documents). Remove the hand-rolled error. - (D) Leaking
secret_internal_flag+ wrong status.TaskOutomits internal fields (module 07 redaction), so it can't leak, and the spec shows only public fields. Setstatus_code=201so the documented success status is correct.
class TaskCreate(BaseModel):
title: str = Field(..., min_length=1, description="Task title")
class TaskOut(BaseModel):
id: int
title: str # no internal flag
@router.post("/tasks", status_code=201, response_model=TaskOut,
responses={422: {"description": "Validation error"}})
async def create_task(body: TaskCreate):
return {"id": 1, "title": body.title, "secret_internal_flag": True} # flag dropped by TaskOut
The lesson of the whole track in one exercise: precise declarations produce a truthful, useful OpenAPI contract; sloppy ones produce a lying spec.
Independent challenge
No code given. Take the /v1/projects sub-API you designed in module 08 and
make its OpenAPI document production-quality: rich info metadata, tag groups
with descriptions, every endpoint with a summary and documented status codes
(including the 404, 409, 412, and 422 you actually return), fully
described request/response schemas with examples, and a documented bearer-auth
security scheme so /docs shows an Authorize button and padlocks. Then export
/openapi.json, and write a short note on whether you'd keep this project
code-first or move it spec-first, justified against the tradeoffs in
this module. Reach back to module 06's consistent error envelope and make sure
your error responses are documented as a shared schema (defined once in
components.schemas, referenced everywhere), not re-described per endpoint.
Hint
Define one ErrorResponse Pydantic model matching your envelope and reference
it in every operation's responses={...} — FastAPI will place it once in
components.schemas and $ref it, exactly the reuse OpenAPI's components
section exists for. Use the HTTPBearer security utility so the scheme is
registered automatically rather than hand-editing the spec.
Common mistakes & troubleshooting
- Returning raw dicts instead of
response_model. The spec can't describe the response; clients get an opaque schema. Always declare the output model. - Untyped request bodies (
body: dict). No request schema, no real validation. Use a Pydantic model. - Confusing OpenAPI (the spec) with Swagger (the tools). OpenAPI is the standard; Swagger UI/Editor/Codegen are tooling built on it.
- Confusing
info.versionwith the URL version. One documents your API release; the other (/v1) is the routing version. Set both deliberately. - Undocumented status codes. If you return
404/409/412, add them toresponses=so the contract matches reality. - Fighting the generator by hand-editing the spec. Enrich via
tags,summary,Fieldmetadata, andresponses=; overrideapp.openapi()only for truly custom needs. - Never exporting/snapshotting the spec. Without a committed
openapi.json, breaking changes slip through code review. Snapshot it for contract testing. - Assuming 3.0 when you're on 3.1. Nullable handling and
examplesdiffer; know which version FastAPI emits and which your downstream tools accept.
Checkpoint quiz
Write down your answer to each question before expanding it — checking without attempting first is the single easiest way to fool yourself into thinking you've learned this.
- What's the difference between "OpenAPI" and "Swagger," and how did the naming come about?
- Name the top-level sections of an OpenAPI document and what each holds.
- FastAPI generates the spec automatically — so what did you do across this track that determines whether that generated spec is accurate or misleading?
- What three URLs does FastAPI serve for the spec and its UIs, and what's at each?
- Give two concrete things a machine-readable OpenAPI document lets tools do that you'd otherwise do by hand.
- Contrast code-first and spec-first development: what does each optimize for, and when would you choose spec-first?
- How does declaring auth with a FastAPI security utility (e.g.
HTTPBearer) affect the generated spec, and why is that useful to clients?
Answers
- OpenAPI is the specification (standard for describing HTTP APIs); Swagger is the tooling (Swagger UI, Editor, Codegen). The project started as Swagger in 2011; in 2015 the spec was donated to the Linux Foundation and renamed the OpenAPI Specification, while the Swagger name stayed on the tools.
openapi(version),info(metadata: title/version/description),servers(base URLs),paths(endpoints with methods, params, request bodies, responses),components(reusableschemas,securitySchemes),security(which schemes apply), and per-operationresponseskeyed by status code.- Being precise: declaring Pydantic request/response models
(
response_model),Fieldconstraints, explicit status codes andresponses=, separate*Outmodels for redaction. Those declarations are the spec — sloppy/untyped code yields an opaque or lying spec. /openapi.json(the raw OpenAPI document),/docs(interactive Swagger UI, try-it-out),/redoc(clean reference documentation).- Any two of: generate interactive docs; generate typed client SDKs in many languages; generate a mock server for parallel frontend development; import into Postman/Insomnia; run contract tests / gateway validation against the spec.
- Code-first optimizes for speed and no code/spec drift (the code is the contract), but the contract is a late side effect. Spec-first optimizes for up-front, cross-team agreement — the contract is reviewed and mockable before code exists. Choose spec-first for public/multi-team APIs where the contract must be negotiated before implementation.
- It both enforces the auth and registers a security scheme in
components.securitySchemeswith asecurityrequirement on protected operations, so/docsshows an Authorize button and padlocks. Clients (and generated SDKs) then know exactly which endpoints need a token and how to supply it.
Further reading & sources
- OpenAPI Specification (spec.openapis.org) - the authoritative current specification (OAS 3.1) describing every document section.
- FastAPI — First Steps / interactive docs - how FastAPI serves
/docs,/redoc, and/openapi.jsonout of your code. - FastAPI — Metadata and Docs URLs - enriching the spec with
title,version,tags, and operation metadata. - FastAPI — Path Operation Configuration - documenting
summary,description,responses, and status codes. - swagger.io — What is OpenAPI? - the OpenAPI-vs-Swagger history and the tooling ecosystem around the spec.
- JSON Schema - the schema standard OpenAPI 3.1 aligns with and that Pydantic emits.
Free PDF notes
Download these notes as a printable PDF, with every diagram and interview questions: SD-C-022: API Contracts & OpenAPI. Each section in one file: Phase 2, APIs (PDF).
Next
10-capstone-project — time to synthesize everything: build a complete, validated, middleware-chained, RESTful API with pagination/search/sort/filter, consistent errors, and an auto-generated OpenAPI spec, from an empty folder.