REST API Documentation with OpenAPI 3.1
Write machine-readable API documentation with OpenAPI 3.1. Learn the spec structure, Swagger UI setup, code generation, and best practices for keeping docs in sync.
What you'll learn
- ✓The structure of an OpenAPI 3.1 specification document
- ✓How to define paths, parameters, request bodies, and responses
- ✓How to set up Swagger UI to serve interactive docs
- ✓How to generate client SDKs and server stubs from your spec
- ✓Strategies for keeping documentation in sync with code
Prerequisites
- •Basic REST API design knowledge
- •Familiarity with JSON or YAML
Good API documentation is the difference between developers adopting your API in an afternoon or abandoning it after an hour. OpenAPI (formerly Swagger) gives you a machine-readable specification that powers interactive docs, client SDK generation, and automated testing — all from a single YAML file.
What is OpenAPI?
OpenAPI is a specification format for describing REST APIs. The current version is 3.1, which aligns with JSON Schema 2020-12. Your OpenAPI document describes every endpoint, parameter, request body, response, and authentication scheme your API supports.
The spec can be written in YAML or JSON. YAML is more readable, so it is the standard choice.
Anatomy of an OpenAPI 3.1 document
openapi: 3.1.0
info:
title: Bookstore API
description: A REST API for managing books and authors.
version: 1.2.0
contact:
name: API Support
email: api@example.com
license:
name: MIT
url: https://opensource.org/licenses/MIT
servers:
- url: https://api.bookstore.example.com/v1
description: Production
- url: https://staging-api.bookstore.example.com/v1
description: Staging
paths:
/books:
get:
summary: List all books
operationId: listBooks
tags: [Books]
parameters:
- $ref: '#/components/parameters/PageLimit'
- $ref: '#/components/parameters/PageCursor'
- name: genre
in: query
schema:
type: string
enum: [fiction, non-fiction, science, history]
responses:
'200':
description: A paginated list of books
content:
application/json:
schema:
$ref: '#/components/schemas/BookList'
'401':
$ref: '#/components/responses/Unauthorized'
post:
summary: Create a new book
operationId: createBook
tags: [Books]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateBookRequest'
example:
title: "The Pragmatic Programmer"
author_id: "auth_001"
isbn: "978-0135957059"
genre: "non-fiction"
price: 49.99
responses:
'201':
description: Book created
content:
application/json:
schema:
$ref: '#/components/schemas/Book'
'422':
$ref: '#/components/responses/ValidationError'
/books/{bookId}:
get:
summary: Get a book by ID
operationId: getBook
tags: [Books]
parameters:
- name: bookId
in: path
required: true
schema:
type: string
pattern: '^book_[a-z0-9]{8}$'
responses:
'200':
description: A single book
content:
application/json:
schema:
$ref: '#/components/schemas/Book'
'404':
$ref: '#/components/responses/NotFound'
Components: reusable schemas
The components section holds reusable definitions. This is where you define your data models:
components:
schemas:
Book:
type: object
required: [id, title, author_id, isbn, genre, price, created_at]
properties:
id:
type: string
example: "book_a1b2c3d4"
title:
type: string
maxLength: 500
example: "The Pragmatic Programmer"
author_id:
type: string
example: "auth_001"
isbn:
type: string
pattern: '^\d{3}-\d{10}$'
example: "978-0135957059"
genre:
type: string
enum: [fiction, non-fiction, science, history]
price:
type: number
minimum: 0
example: 49.99
created_at:
type: string
format: date-time
CreateBookRequest:
type: object
required: [title, author_id, isbn, genre, price]
properties:
title:
type: string
maxLength: 500
author_id:
type: string
isbn:
type: string
pattern: '^\d{3}-\d{10}$'
genre:
type: string
enum: [fiction, non-fiction, science, history]
price:
type: number
minimum: 0
BookList:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Book'
pagination:
$ref: '#/components/schemas/CursorPagination'
links:
$ref: '#/components/schemas/PaginationLinks'
CursorPagination:
type: object
properties:
has_more:
type: boolean
next_cursor:
type: string
nullable: true
PaginationLinks:
type: object
properties:
self:
type: string
format: uri
next:
type: string
format: uri
nullable: true
first:
type: string
format: uri
ProblemDetail:
type: object
required: [type, title, status]
properties:
type:
type: string
format: uri
title:
type: string
status:
type: integer
detail:
type: string
instance:
type: string
parameters:
PageLimit:
name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
PageCursor:
name: cursor
in: query
schema:
type: string
responses:
Unauthorized:
description: Authentication required
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetail'
NotFound:
description: Resource not found
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetail'
ValidationError:
description: Validation failed
content:
application/problem+json:
schema:
allOf:
- $ref: '#/components/schemas/ProblemDetail'
- type: object
properties:
errors:
type: array
items:
type: object
properties:
field:
type: string
message:
type: string
code:
type: string
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
Serving interactive docs with Swagger UI
Swagger UI renders your OpenAPI spec as an interactive HTML page where developers can read docs and test endpoints directly in the browser.
Option 1: Express middleware
import express from 'express';
import swaggerUi from 'swagger-ui-express';
import YAML from 'yaml';
import fs from 'fs';
const app = express();
const spec = YAML.parse(fs.readFileSync('./openapi.yaml', 'utf-8'));
app.use('/docs', swaggerUi.serve, swaggerUi.setup(spec, {
customCss: '.swagger-ui .topbar { display: none }',
customSiteTitle: 'Bookstore API Docs',
}));
// Serve the raw spec for code generators
app.get('/openapi.yaml', (req, res) => {
res.type('text/yaml').sendFile('./openapi.yaml', { root: process.cwd() });
});
Option 2: Static HTML
If you do not want a runtime dependency, use the Swagger UI CDN:
<!DOCTYPE html>
<html>
<head>
<title>API Docs</title>
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script>
SwaggerUIBundle({
url: '/openapi.yaml',
dom_id: '#swagger-ui',
deepLinking: true,
});
</script>
</body>
</html>
Code generation
One of the biggest benefits of OpenAPI is automated code generation. The spec drives client SDKs, server stubs, and even mock servers.
Generate a TypeScript client
npx @openapitools/openapi-generator-cli generate \
-i openapi.yaml \
-g typescript-fetch \
-o ./generated/client \
--additional-properties=supportsES6=true,typescriptThreePlus=true
This generates a fully typed client:
import { BooksApi, Configuration } from './generated/client';
const api = new BooksApi(new Configuration({
basePath: 'https://api.bookstore.example.com/v1',
accessToken: 'your-jwt-token',
}));
// Fully typed — IDE autocomplete for parameters and response
const bookList = await api.listBooks({ genre: 'fiction', limit: 10 });
console.log(bookList.data[0].title);
Generate a mock server
npx @stoplight/prism-cli mock openapi.yaml --port 4010
Prism reads your spec and returns example responses. Frontend teams can start building before the backend is ready.
Keeping docs in sync
The biggest challenge with OpenAPI is keeping the spec accurate as the code evolves. Three strategies:
1. Spec-first (recommended)
Write the OpenAPI spec first, then generate server stubs and client code. The spec is the source of truth.
Pros: Forces design review before implementation. Generated code always matches the spec. Cons: Requires discipline to update the spec before changing code.
2. Code-first with annotations
Use decorators or annotations in your code to generate the spec automatically:
// Using express-openapi or tsoa
/**
* @openapi
* /books:
* get:
* summary: List all books
* responses:
* 200:
* description: Success
*/
app.get('/books', listBooks);
Pros: Spec cannot drift from code. Cons: Comments are hard to review and can get stale.
3. Contract testing
Write tests that validate your API responses against the OpenAPI spec:
import { describe, it, expect } from 'vitest';
import OpenAPIValidator from 'openapi-response-validator';
import spec from './openapi.json';
describe('GET /books', () => {
it('response matches OpenAPI spec', async () => {
const res = await fetch('http://localhost:3000/api/books');
const body = await res.json();
const validator = new OpenAPIValidator({
responses: spec.paths['/books'].get.responses,
});
const errors = validator.validateResponse(200, body);
expect(errors).toBeUndefined();
});
});
Best practices
- Use
operationIdon every path. It drives code generation naming. - Add
examplesto schemas. They appear in Swagger UI and power mock servers. - Use
$refaggressively. Reuse schemas, parameters, and responses. - Tag your endpoints. Tags group operations in Swagger UI navigation.
- Version your spec. Use
info.versionand keep a changelog. - Validate your spec in CI. Use
spectral lint openapi.yamlto catch errors before merge.
# Add to your CI pipeline
npx @stoplight/spectral-cli lint openapi.yaml
Summary
OpenAPI 3.1 is the standard for REST API documentation. Write your spec in YAML, serve it with Swagger UI for interactive exploration, and use code generators to create typed clients. Choose spec-first development when possible, and add contract tests as a safety net. A well-maintained OpenAPI spec is not just documentation — it is an executable contract that keeps your API, your clients, and your team aligned.
Related articles
- REST APIs REST API Error Handling Standards: RFC 7807 & Beyond
Design consistent, machine-readable error responses using RFC 7807 Problem Details, custom error codes, and structured error envelopes that clients can actually parse.
- REST APIs REST API Hypermedia & HATEOAS: HAL, JSON:API & Practical Examples
Implement HATEOAS in your REST API using HAL, JSON:API, and custom link formats. Learn when hypermedia helps, when it hurts, and how to add it incrementally.
- REST APIs REST API Pagination Best Practices: Offset, Cursor & Keyset
Choose the right pagination strategy for your REST API. Compare offset, cursor, and keyset pagination with real examples, HATEOAS links, and performance trade-offs.
- REST APIs REST API Rate Limiting: Token Bucket, Sliding Window & Implementation
Protect your REST API from abuse with rate limiting. Learn token bucket, sliding window, and fixed window algorithms with real implementation examples and standard headers.