OpenAPI and Swagger: A Practical Tutorial to Document Your REST API
Step-by-step tutorial to design, write, validate, and publish API docs using OpenAPI and Swagger UI, with examples and best practices.
Image used for representation purposes only.
Overview
OpenAPI (formerly Swagger) is the industry-standard way to describe HTTP APIs in a language‑agnostic, machine‑readable format. With a single OpenAPI document (YAML or JSON), you can:
- Generate interactive documentation with Swagger UI
- Validate request/response shapes automatically
- Mock endpoints for early testing
- Generate client SDKs and server stubs
- Improve collaboration between product, backend, frontend, QA, and DevOps
This tutorial walks you through a pragmatic, design-first workflow: author a minimal spec, refine it with reusable components, render it with Swagger UI, secure it, validate it in CI, and generate clients.
Prerequisites and mental model
- You know basic HTTP (methods, status codes, headers) and JSON.
- You can run commands in a terminal and install npm packages if you choose the Node.js examples.
- You’ll maintain a single source of truth: openapi.yaml. Tooling reads from it to produce docs, mocks, and code.
Key concepts you’ll use:
- Version field: openapi: 3.1.0 (or 3.0.x if your tooling requires it)
- Paths: endpoints under /paths
- Operations: HTTP verbs (get, post, etc.) on a path
- Components: reusable schemas, parameters, responses, securitySchemes
- Content: media types like application/json with schemas
Quickstart: a minimal OpenAPI document
Create a file named openapi.yaml at your project root with the minimal “Todo API” below. It’s intentionally compact to get you productive fast; we’ll expand it in the next sections.
openapi: 3.1.0
info:
title: Todo API
summary: Simple CRUD for tasks
version: 1.0.0
servers:
- url: https://api.example.com/v1
paths:
/todos:
get:
summary: List todos
tags: [Todos]
responses:
'200':
description: Array of todos
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Todo'
post:
summary: Create a todo
tags: [Todos]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TodoCreate'
responses:
'201':
description: Created
headers:
Location:
description: URL of the new resource
schema: { type: string, format: uri }
content:
application/json:
schema:
$ref: '#/components/schemas/Todo'
/todos/{id}:
parameters:
- $ref: '#/components/parameters/TodoId'
get:
summary: Get a todo
tags: [Todos]
responses:
'200':
description: The todo
content:
application/json:
schema:
$ref: '#/components/schemas/Todo'
'404': { description: Not found }
patch:
summary: Update a todo
tags: [Todos]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TodoUpdate'
responses:
'200':
description: Updated
content:
application/json:
schema:
$ref: '#/components/schemas/Todo'
'404': { description: Not found }
delete:
summary: Delete a todo
tags: [Todos]
responses:
'204': { description: No Content }
'404': { description: Not found }
components:
parameters:
TodoId:
name: id
in: path
required: true
description: Todo identifier
schema: { type: string, pattern: '^[A-Za-z0-9_-]{8,}$' }
schemas:
Todo:
type: object
required: [id, title, completed]
properties:
id: { type: string, example: 'todo_1234abcd' }
title: { type: string, example: 'Write OpenAPI tutorial' }
completed: { type: boolean, default: false }
dueDate: { type: string, format: date-time, nullable: true }
priority:
type: string
enum: [low, medium, high]
default: medium
TodoCreate:
type: object
required: [title]
properties:
title: { type: string }
dueDate: { type: string, format: date-time, nullable: true }
priority: { $ref: '#/components/schemas/Todo/properties/priority' }
TodoUpdate:
type: object
properties:
title: { type: string }
completed: { type: boolean }
dueDate: { type: string, format: date-time, nullable: true }
priority: { $ref: '#/components/schemas/Todo/properties/priority' }
Tip: Prefer YAML for readability. Keep examples close to schemas so your docs show realistic payloads.
Validate early with editor and linters
- Use a visual editor to get instant validation and an interactive preview. Paste openapi.yaml into an editor to catch syntax and schema issues.
- Add a linter to enforce style and consistency. A common choice is Spectral.
# Validate and lint your spec locally
npx @stoplight/spectral lint openapi.yaml
Linters catch ambiguous summaries, undocumented response codes, missing examples, and inconsistent naming.
Render interactive docs with Swagger UI (Node.js example)
Swagger UI turns openapi.yaml into a beautiful website with “Try it out” buttons.
- Install dependencies:
npm i express swagger-ui-express js-yaml
- Serve Swagger UI:
// server.js
const express = require('express');
const fs = require('fs');
const yaml = require('js-yaml');
const swaggerUi = require('swagger-ui-express');
const app = express();
const spec = yaml.load(fs.readFileSync('./openapi.yaml', 'utf8'));
app.use('/docs', swaggerUi.serve, swaggerUi.setup(spec, {
explorer: true,
customSiteTitle: 'Todo API Docs',
}));
app.listen(3000, () => console.log('Docs at http://localhost:3000/docs'));
- Start the server and open the docs. You’ll see endpoints, schemas, and sample payloads. Click “Try it out” to send real requests to your API server (configure the server URL under servers in your spec).
Alternative integrations exist for many platforms (e.g., Spring Boot, ASP.NET Core). The core idea is the same: serve Swagger UI and feed it your OpenAPI document.
Add rich detail that helps users succeed
Great docs are specific. Expand your spec with examples, error models, and headers.
Examples
Use inline examples or named examples for multiple cases.
responses:
'200':
description: The todo
content:
application/json:
schema: { $ref: '#/components/schemas/Todo' }
examples:
typical:
summary: A normal todo
value:
id: todo_1234abcd
title: Write OpenAPI tutorial
completed: false
priority: high
Error model
Document error shapes consistently and reuse them.
components:
schemas:
Error:
type: object
required: [code, message]
properties:
code: { type: string, example: 'NOT_FOUND' }
message: { type: string, example: 'Todo was not found' }
details: { type: array, items: { type: string } }
Then reference it in responses:
'404':
description: Not found
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
Pagination and filtering
Standardize pagination parameters and response headers.
components:
parameters:
Page:
name: page
in: query
schema: { type: integer, minimum: 1, default: 1 }
PageSize:
name: pageSize
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
Use them:
/todos:
get:
parameters:
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/PageSize'
responses:
'200':
description: Paginated results
headers:
X-Total-Count:
description: Total items available
schema: { type: integer }
Authentication and authorization
OpenAPI supports multiple auth mechanisms. Define under components.securitySchemes and apply via security.
Bearer (JWT) example
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- BearerAuth: []
This applies to all operations. Override per-operation by setting security at the operation level.
API key example (header)
components:
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
OAuth2 example (Authorization Code)
components:
securitySchemes:
OAuth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://auth.example.com/oauth/authorize
tokenUrl: https://auth.example.com/oauth/token
scopes:
todos:read: Read todos
todos:write: Create and update todos
Document required scopes on each operation with security: [{ OAuth2: [“todos:read”] }]. Provide a test token or a sandbox tenant so “Try it out” works.
Organize and reuse effectively
- Schemas: Model request/response bodies; compose with allOf, oneOf, anyOf to express polymorphism.
- Parameters: Reuse path and query parameters to avoid drift.
- Responses: Centralize common responses (401, 404, 422) with examples.
- Tags: Group operations for navigation; add descriptions and externalDocs.
- Servers: List production and staging; use variables for regions.
servers:
- url: https://{region}.api.example.com/v1
variables:
region:
default: us
enum: [us, eu]
Design-first vs code-first
- Design-first: Author the spec before coding. Benefits: early feedback, parallel work, mocks, fewer reworks. Best for new APIs and teams.
- Code-first: Generate specs from annotations or code. Fast for existing services but can produce sparse docs if you don’t enrich them.
Choose one, then automate generation or validation so the spec never drifts from reality.
Add testing and mocking
- Mock server: Use your OpenAPI to spin up a mock that returns example payloads. Great for frontend development before the backend is ready.
- Contract tests: Validate real API responses against schemas. Many test frameworks can read OpenAPI and assert response shapes.
Generate clients and stubs with OpenAPI Generator
Automation saves time and reduces mistakes.
# Install the CLI (on-demand via npx)
npx @openapitools/openapi-generator-cli generate \
-i openapi.yaml \
-g typescript-fetch \
-o ./clients/ts
Popular generators: typescript-fetch, javascript, python, go, java, kotlin, swift, csharp. Treat generated code as scaffolding; don’t hand-edit files you plan to re-generate. Wrap them in a thin layer for app-specific concerns (retries, auth token refresh, logging).
CI/CD: keep your docs healthy
Automate checks so your API contract stays correct.
# 1) Lint for style and anti-patterns
npx @stoplight/spectral lint openapi.yaml
# 2) Validate references ($ref) and structure via a validator
npx openapi-typescript-validate openapi.yaml || exit 1
# 3) Bundle (resolve $ref) for distribution
npx @redocly/cli bundle openapi.yaml -o dist/openapi.bundled.yaml
Then publish your docs: host Swagger UI behind /docs in each environment, or deploy a static site that reads the bundled spec from a CDN. Gate production deploys on passing contract tests.
Common pitfalls and how to avoid them
- Undocumented error codes: List 4xx/5xx with an Error schema and examples.
- Vague summaries: Start with a verb and be specific (e.g., “List todos” vs “Get data”).
- Inconsistent naming: Pick a case convention (e.g., snake_case for JSON fields) and enforce with a linter.
- Missing examples: Without examples, users guess. Provide at least one success and one error example per operation.
- Overloading endpoints: Prefer clear, resource-oriented paths (/todos/{id}) over RPC-like verbs (/updateTodo).
- Schema drift: Add contract tests so actual responses match documented schemas.
- Authentication gaps: Document how to get tokens, scopes, and example credentials for testing.
Versioning and lifecycle
- Start with semantic versions in info.version (e.g., 1.0.0).
- Use servers to expose /v1, /v2 when you make breaking changes.
- Mark deprecated operations with deprecated: true and document sunset timelines.
- Provide a changelog that references spec diffs for consumers.
A concise checklist
- Spec validates (syntax and structure) and lints cleanly
- Every operation has: summary, tags, requestBody (if applicable), responses with schemas and examples
- Common errors modeled and reused
- Authentication defined and testable in Swagger UI
- Pagination/filtering documented where relevant
- CI bundles and publishes docs; contract tests run in CI
- Clients generated (optional) and wrapped in app conventions
Where to go next
- Expand your spec with webhooks for event-driven patterns
- Use oneOf/anyOf with discriminators for more complex domain models
- Add links to relate operations (e.g., POST /todos returns a link to GET /todos/{id})
- Introduce schema examples and test fixtures from the same source to keep docs and tests aligned
With a single, well-structured OpenAPI document and Swagger UI, you’ll deliver clear, testable, and delightful API experiences—while giving your team automation superpowers across the lifecycle.
Related Posts
The API Versioning Playbook: Best Practices, Patterns, and Pitfalls
A practical playbook for API versioning: strategies, SemVer, backward compatibility, deprecation, testing, and rollout patterns for stable, evolving APIs.
Migrating to Dart 3 Null Safety in Flutter: A Practical Guide
Step-by-step guide to migrate a Flutter codebase to Dart 3’s sound null safety with commands, patterns, pitfalls, and checklists.
React Hooks Best Practices: Patterns for Clean, Correct, and Performant Components
A practical guide to React Hooks best practices: state, effects, memoization, refs, custom hooks, concurrency, testing, and pitfalls—with examples.