How to Document Your API With OpenAPI 3.0 (Swagger Tutorial)
Write clear, interactive API docs using OpenAPI 3.0 and Swagger UI. Includes full YAML examples, Express setup, spec-first vs code-first comparison, and auto-generation tips.
Get more content like this on Telegram!
Daily AI tips, notes & resources — free
Good API documentation is the difference between an API that gets adopted and one that gets abandoned. I've integrated with plenty of APIs over the years, and the ones with clear, interactive documentation — where I can try an endpoint before writing a single line of code — are the ones I actually enjoy using.
OpenAPI 3.0 (formerly Swagger) is the industry standard for documenting REST APIs. This guide walks through writing an OpenAPI spec from scratch, serving it with Swagger UI in an Express app, and deciding between spec-first and code-first approaches. By the end, you'll have interactive documentation your API consumers will actually appreciate.
What OpenAPI 3.0 Actually Is
OpenAPI is a specification format — a structured way to describe your API in YAML or JSON. It defines your endpoints, request/response schemas, authentication methods, and examples in a machine-readable format.
"Machine-readable" is the key phrase. A valid OpenAPI document can:
- Render as interactive Swagger UI documentation
- Generate client SDKs in 40+ languages
- Drive automated API contract testing
- Power type-safe TypeScript clients
Version 3.0 (released 2017) improved significantly on Swagger 2.0. The main additions: components for reusable schemas, oneOf/anyOf/allOf for polymorphism, better security definitions, and proper support for callbacks and links. If you're starting fresh, use 3.0 (or 3.1 if you want full JSON Schema support). See the official OpenAPI 3.0 specification for the complete reference.
Basic OpenAPI 3.0 Structure
Every OpenAPI document has the same top-level structure:
openapi: 3.0.3
info:
title: Blog API
description: A simple blogging API for demonstration
version: 1.0.0
contact:
name: API Support
email: support@example.com
license:
name: MIT
servers:
- url: https://api.example.com/v1
description: Production
- url: http://localhost:3000/v1
description: Local development
tags:
- name: posts
description: Blog post operations
- name: users
description: User management
- name: auth
description: Authentication endpoints
paths:
# Endpoint definitions go here
components:
# Reusable schemas, responses, parameters, security schemes
The components section is one of the biggest improvements in 3.0. Instead of repeating schema definitions everywhere, you define them once and reference them with $ref.
Writing Paths and Operations
Let's document a blog posts API:
paths:
/posts:
get:
tags:
- posts
summary: List all posts
description: Returns a paginated list of published blog posts.
operationId: listPosts
parameters:
- name: page
in: query
description: Page number (1-indexed)
required: false
schema:
type: integer
minimum: 1
default: 1
- name: limit
in: query
description: Number of posts per page
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 20
- name: tag
in: query
description: Filter by tag
required: false
schema:
type: string
responses:
'200':
description: Paginated list of posts
content:
application/json:
schema:
$ref: '#/components/schemas/PostListResponse'
example:
data:
- id: "post_abc123"
title: "Getting Started with OpenAPI"
excerpt: "Learn how to document your API..."
author:
id: "user_xyz789"
name: "Jane Smith"
createdAt: "2026-06-01T10:00:00Z"
meta:
page: 1
limit: 20
total: 142
'400':
$ref: '#/components/responses/BadRequest'
'500':
$ref: '#/components/responses/InternalError'
post:
tags:
- posts
summary: Create a new post
operationId: createPost
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreatePostInput'
example:
title: "My First Post"
content: "This is the post content..."
tags: ["tutorial", "beginners"]
responses:
'201':
description: Post created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Post'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
/posts/{postId}:
get:
tags:
- posts
summary: Get a single post
operationId: getPost
parameters:
- name: postId
in: path
required: true
description: Unique post identifier
schema:
type: string
responses:
'200':
description: Post found
content:
application/json:
schema:
$ref: '#/components/schemas/Post'
'404':
$ref: '#/components/responses/NotFound'
delete:
tags:
- posts
summary: Delete a post
operationId: deletePost
security:
- bearerAuth: []
parameters:
- name: postId
in: path
required: true
schema:
type: string
responses:
'204':
description: Post deleted (no content)
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
Components: Reusable Schemas and Responses
The real power of OpenAPI 3.0 is in components. Define your schemas and responses once, reference them everywhere:
components:
# ─────────────────────────────────
# Schemas
# ─────────────────────────────────
schemas:
Post:
type: object
required:
- id
- title
- content
- author
- createdAt
properties:
id:
type: string
description: Unique post identifier
example: "post_abc123"
title:
type: string
minLength: 3
maxLength: 500
example: "How to Use OpenAPI 3.0"
content:
type: string
description: Full post content (Markdown)
excerpt:
type: string
maxLength: 300
nullable: true
tags:
type: array
items:
type: string
example: ["tutorial", "api"]
author:
$ref: '#/components/schemas/UserSummary'
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
nullable: true
UserSummary:
type: object
required:
- id
- name
properties:
id:
type: string
example: "user_xyz789"
name:
type: string
example: "Jane Smith"
avatarUrl:
type: string
format: uri
nullable: true
CreatePostInput:
type: object
required:
- title
- content
properties:
title:
type: string
minLength: 3
maxLength: 500
content:
type: string
minLength: 10
tags:
type: array
items:
type: string
maxItems: 10
PostListResponse:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Post'
meta:
type: object
properties:
page:
type: integer
limit:
type: integer
total:
type: integer
ErrorResponse:
type: object
required:
- error
- message
properties:
error:
type: string
example: "VALIDATION_ERROR"
message:
type: string
example: "Title must be at least 3 characters"
details:
type: array
items:
type: object
properties:
field:
type: string
message:
type: string
# ─────────────────────────────────
# Reusable Responses
# ─────────────────────────────────
responses:
BadRequest:
description: Invalid request parameters
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
Unauthorized:
description: Authentication required
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error: "UNAUTHORIZED"
message: "Valid authentication token required"
Forbidden:
description: Insufficient permissions
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
InternalError:
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
# ─────────────────────────────────
# Security Schemes
# ─────────────────────────────────
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: JWT token obtained from the /auth/login endpoint
Serving Swagger UI with Express
Now let's wire this spec into an Express API:
npm install swagger-ui-express yaml js-yaml
// app.js
const express = require('express');
const swaggerUi = require('swagger-ui-express');
const fs = require('fs');
const yaml = require('js-yaml');
const path = require('path');
const app = express();
app.use(express.json());
// Load the OpenAPI spec
const specPath = path.join(__dirname, 'openapi.yaml');
const apiSpec = yaml.load(fs.readFileSync(specPath, 'utf8'));
// Serve Swagger UI
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(apiSpec, {
customSiteTitle: 'Blog API Docs',
customCss: '.swagger-ui .topbar { background-color: #1a1a2e; }',
swaggerOptions: {
persistAuthorization: true, // Remember auth token between page refreshes
displayRequestDuration: true,
filter: true,
tryItOutEnabled: true
}
}));
// Serve raw spec (useful for SDK generation)
app.get('/api-docs.json', (req, res) => res.json(apiSpec));
app.get('/api-docs.yaml', (req, res) => {
res.type('text/yaml');
res.send(fs.readFileSync(specPath, 'utf8'));
});
// Your actual routes
app.get('/v1/posts', async (req, res) => {
// ... implementation
});
app.listen(3000, () => {
console.log('Server running at http://localhost:3000');
console.log('API docs at http://localhost:3000/api-docs');
});
Visit http://localhost:3000/api-docs and you have a fully interactive API explorer. Developers can try every endpoint, see request/response schemas, and authenticate — all without writing any client code.
Code-First vs Spec-First Comparison
This is genuinely a workflow choice, not just a technical one:
| Aspect | Spec-First | Code-First |
|---|---|---|
| API design quality | Higher (forced intentionality) | Lower (implementation leaks in) |
| Parallel development | Yes (frontend can start from spec) | No (need code before spec) |
| Initial effort | Higher | Lower |
| Existing codebase | Harder to retrofit | Easier to add to existing code |
| Spec accuracy | Always accurate | Can drift from implementation |
| Tooling examples | Swagger Editor, Stoplight | tsoa, express-openapi-validator |
| Best for | New APIs, teams >2 people | Solo devs, existing APIs |
For auto-generating spec from Express routes, express-openapi-validator lets you annotate routes with JSDoc-style comments. The FastAPI tutorial is worth reading here — FastAPI auto-generates OpenAPI docs from Python type hints, which is a genuinely elegant code-first approach worth knowing about.
Validating Requests Against Your Spec
One often-overlooked benefit: you can use your OpenAPI spec to validate incoming requests automatically:
npm install express-openapi-validator
const OpenApiValidator = require('express-openapi-validator');
app.use(
OpenApiValidator.middleware({
apiSpec: './openapi.yaml',
validateRequests: true,
validateResponses: process.env.NODE_ENV !== 'production', // Dev only
ignorePaths: /\/api-docs/
})
);
// Validation errors are automatically caught and formatted
app.use((err, req, res, next) => {
if (err.status && err.errors) {
return res.status(err.status).json({
error: 'VALIDATION_ERROR',
message: err.message,
details: err.errors
});
}
next(err);
});
Now your spec serves three purposes: documentation, interactive explorer, and request validation. One source of truth for all three.
For authentication patterns referenced in the spec's securitySchemes, see API authentication guide. For how OpenAPI docs fit into a broader API strategy alongside REST vs GraphQL trade-offs, REST vs GraphQL guide covers the decision framework.
If you're building the backend that needs documenting, FastAPI tutorial and Node.js file upload Multer both touch on API design patterns that translate directly to OpenAPI schemas.
Conclusion
An OpenAPI spec is a living document that pays dividends every time someone integrates with your API. Write it spec-first for new projects, use it to validate requests in development, serve it via Swagger UI for interactive documentation, and share the raw YAML endpoint for anyone who wants to generate a client SDK.
The upfront investment is maybe two hours for a typical CRUD API. The return — fewer support questions, faster frontend integration, auto-generated SDKs, contract tests — compounds indefinitely.
Start with your most-used endpoints, define the schemas in components, and build from there. Your future self (and every developer who integrates with your API) will appreciate it.
Frequently Asked Questions
What is the difference between OpenAPI and Swagger?
Swagger was the original name for both the specification and the tooling. In 2016, the Swagger specification was donated to the OpenAPI Initiative and renamed OpenAPI Specification (OAS). Swagger is now the brand name for the tools built by SmartBear — Swagger UI, Swagger Editor, Swagger Codegen. OpenAPI is the specification itself. When someone says "Swagger docs", they usually mean API documentation written in OpenAPI format and rendered by Swagger UI.
Should I write OpenAPI spec by hand or generate it from code?
For a new API, spec-first is strongly recommended: write the OpenAPI YAML before writing any code. This forces you to design the API contract deliberately, enables frontend and backend to work in parallel from the spec, and results in cleaner, more consistent APIs. Code-first (generating spec from annotations or decorators) is useful for documenting existing APIs where the code is already written. The generated output often needs cleanup — especially descriptions, examples, and error response schemas — which spec-first gives you for free.
Can I use OpenAPI 3.0 to generate client SDKs automatically?
Yes, and it's one of the most valuable things about having a proper OpenAPI spec. Tools like openapi-generator and swagger-codegen can generate typed client SDKs in 40+ languages from your spec. This means your frontend team gets a TypeScript client with full type safety, your mobile team gets a Swift/Kotlin client, and third-party developers can generate clients in their preferred language. A well-written spec becomes a self-service SDK factory.
Frequently Asked Questions
AiTechWorlds Team
✓ Verified WriterThe AiTechWorlds team is passionate about AI, technology, and education. We create high-quality, research-backed content to help you learn, grow, and succeed in the modern digital world.
Related Articles
GraphQL vs REST: Which Should You Learn First? (2026 Guide)
Deciding between GraphQL and REST as a junior developer? Compare learning curves, hiring demand, and real use cases to pick the right starting point.
GraphQL vs REST: Real-World Performance Test and Benchmarks
Real benchmark results comparing GraphQL and REST APIs on response time, payload size, and network requests — with honest analysis of over-fetching, under-fetching, and when each wins.
How to Deploy a LangChain App as a FastAPI REST Endpoint
Serve a LangChain app as a production FastAPI REST endpoint with streaming, async chains, error handling, and Docker deployment — full Python code included.
Build a REST API With Node.js + Express + MongoDB (Full Tutorial)
Step-by-step tutorial to build a production-ready CRUD REST API using Node.js, Express, and MongoDB with models, routes, controllers, and error handling.