04 · OpenAPI/Swagger Spec Basics¶
OpenAPI (formerly "Swagger") is a machine-readable specification format for describing a REST API's endpoints, request/response shapes, and auth requirements in YAML or JSON. Once an API has an OpenAPI document, tools can auto-generate interactive docs, client SDKs, server stubs, and test suites from it — the spec becomes a single source of truth instead of hand-written docs drifting out of sync with the actual code.
Anatomy of an OpenAPI document¶
openapi: 3.0.3
info:
title: Bookshelf API
version: 1.0.0
description: A simple API for managing a book collection.
servers:
- url: https://api.example.com/v1
paths:
/books:
get:
summary: List books
parameters:
- name: limit
in: query
schema: { type: integer, default: 20 }
- name: genre
in: query
schema: { type: string }
responses:
'200':
description: A paginated list of books
content:
application/json:
schema:
type: object
properties:
data:
type: array
items: { $ref: '#/components/schemas/Book' }
meta:
type: object
properties:
total: { type: integer }
post:
summary: Create a book
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/NewBook' }
responses:
'201':
description: Book created
content:
application/json:
schema: { $ref: '#/components/schemas/Book' }
'422':
description: Validation error
/books/{id}:
get:
summary: Get a single book
parameters:
- name: id
in: path
required: true
schema: { type: integer }
responses:
'200':
description: The book
content:
application/json:
schema: { $ref: '#/components/schemas/Book' }
'404':
description: Book not found
components:
schemas:
Book:
type: object
properties:
id: { type: integer }
title: { type: string }
genre: { type: string }
price: { type: number, format: float }
required: [id, title]
NewBook:
type: object
properties:
title: { type: string }
genre: { type: string }
price: { type: number }
required: [title]
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- bearerAuth: []
Key sections:
info— metadata (title, version, description).servers— base URL(s), useful for switching between staging/prod.paths— every endpoint, HTTP method, parameters, request body, and possible responses.components/schemas— reusable data shapes, referenced with$refto avoid repeating theBookshape in every response that includes one.components/securitySchemes+ top-levelsecurity— how clients authenticate.
Documenting parameters and validation¶
parameters:
- name: price_gte
in: query
schema: { type: number, minimum: 0 }
description: Only return books priced at or above this value.
- name: sort
in: query
schema:
type: string
enum: [title, -title, price, -price, published_date, -published_date]
An enum on the sort parameter documents exactly which values are valid
— and tools like Swagger UI render it as a dropdown, which doubles as
built-in input validation guidance for anyone testing the API by hand.
Generating docs and clients from the spec¶
Once openapi.yaml exists, several things become free:
# Serve interactive docs (Swagger UI) from the spec
npx @redocly/cli preview-docs openapi.yaml
# Validate the spec itself is well-formed
npx @redocly/cli lint openapi.yaml
# Generate a TypeScript client
npx openapi-typescript openapi.yaml -o api-types.ts
Swagger UI turns the YAML into a page where every endpoint is documented and directly callable ("Try it out") from the browser — no separate Postman collection to maintain.
Spec-first vs code-first¶
Code-first: write the API, then generate the OpenAPI spec from
annotations in the code (e.g. FastAPI does this automatically from Python
type hints; Spring uses springdoc-openapi). The spec always matches the
code because it's derived from it.
# FastAPI: spec is generated automatically from this code
@app.get("/books/{id}", response_model=Book)
def get_book(id: int):
...
Spec-first: write openapi.yaml first, review/agree on it with
consumers, then generate server stubs and implement against them. This
front-loads API design discussions before any code exists — valuable for
public or cross-team APIs where a breaking rework after the fact is
expensive.
Neither is universally better: code-first is faster for a small team owning both ends; spec-first is safer when the API has external consumers who need to build against a stable contract before the implementation is finished.
Worked example: adding a new endpoint to an existing spec¶
Adding DELETE /books/{id}:
/books/{id}:
delete:
summary: Delete a book
parameters:
- name: id
in: path
required: true
schema: { type: integer }
responses:
'204':
description: Book deleted, no content returned
'404':
description: Book not found
security:
- bearerAuth: []
Running redocly lint after this change catches, e.g., a missing
description or an inconsistent response code before it ships — cheaper
than a consumer discovering the mismatch at runtime.
How It Actually Works¶
An OpenAPI document isn't documentation in the "prose" sense — it's a machine-readable JSON/YAML schema describing every route, parameter, and response shape, which is precisely what lets tooling generate real code and validation from it without a human reading a word.
Given this fragment:
paths:
/books/{id}:
get:
parameters:
- name: id
in: path
schema: { type: integer }
responses:
"200":
content:
application/json:
schema:
type: object
properties:
id: { type: integer }
title: { type: string }
A code generator walks this tree and emits a typed client method
(getBook(id: number): Promise<{id: number, title: string}>) by literally
mapping OpenAPI's type: integer/type: string primitives to the target
language's types — no guessing involved, because the schema is a formal
grammar (built on JSON Schema) that a parser can traverse deterministically.
The same document powers request validation middleware: before your
handler runs, a validator checks the incoming id path param against
type: integer and rejects non-numeric values with 400 automatically —
this is why teams treat the spec as a contract, not just docs: the same
artifact that describes the API can also enforce it at runtime, catching
mismatches between what you documented and what your handler actually
accepts.
Exercise¶
- Write the OpenAPI
pathsentry forPATCH /books/{id}that accepts a partialNewBook-shaped body and returns the updatedBook, plus a404and422response. - Add a
components/schemas/Errorschema describing the error envelope from Level 1's error-handling module, and reference it from every non-2xx response in the spec. - Explain, with a concrete scenario, one situation where spec-first is clearly better than code-first, and one where the reverse is true.
- Why does putting
sortbehind anenumin the spec catch bugs earlier than only validating it in server code?