API Documentation
Front-end, mobile and partner developers need to know every endpoint, field and error of LearnHub. An OpenAPI document describes the API in a machine-readable way — it powers Swagger UI, client generators, mock servers and contract tests.
On this page · 7 min · Advanced
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 7 min
- Difficulty
- Advanced
- Updated
- Oct 28, 2026
Related tutorials
Interview questions
Learning objectives#
- Describe paths, parameters, bodies and responses in OpenAPI 3.1
- Define reusable schemas and a Bearer security scheme
- Serve /openapi.json and Swagger UI from Express
- Generate the spec from route definitions so it never drifts
Explanation#
An OpenAPI document has info (title, version), servers, paths (each URL with its methods), and components (reusable schemas, responses and security schemes). Express :id paths are written {id}.
Each operation lists parameters (path, query, header), requestBody, responses per status code and security requirements. Schemas use JSON Schema: type, required, properties, enum, minimum.
Serve the document at /openapi.json. swagger-ui-express renders interactive docs at /docs: app.use("/docs", swaggerUi.serve, swaggerUi.setup(spec)).
Docs drift when they are written separately from code. Generate them: keep route metadata next to the handler (this lesson), generate from Zod schemas (zod-to-openapi), or write the spec first and validate requests against it (express-openapi-validator).
Document errors (400, 401, 403, 404, 409, 429) with the Problem schema, not just the happy path — that is what client developers need most.
Syntax#
openapi: 3.1.0
info: { title: LearnHub API, version: 1.0.0 }
paths:
/api/courses/{id}:
get:
summary: Get one course
parameters:
- { name: id, in: path, required: true, schema: { type: integer } }
responses:
'200': { description: The course, content: { application/json: { schema: { $ref: '#/components/schemas/Course' } } } }
'404': { description: Not found }
Code example: One route table → Express routes and the OpenAPI spec#
import express from 'express';
const Course = {
type: 'object', required: ['id', 'title', 'level', 'price'],
properties: {
id: { type: 'integer' }, title: { type: 'string', maxLength: 120 },
level: { type: 'string', enum: ['beginner', 'intermediate', 'advanced'] }, price: { type: 'integer', minimum: 0 },
},
};
const Problem = { type: 'object', properties: { title: { type: 'string' }, status: { type: 'integer' }, detail: { type: 'string' } } };
const ref = name => ({ $ref: `#/components/schemas/${name}` });
const courses = [{ id: 1, title: 'Node.js REST APIs', level: 'beginner', price: 499 }];
// Each route carries its own documentation
const routes = [
{ method: 'get', path: '/api/courses', summary: 'List courses',
responses: { 200: ['Courses', { type: 'array', items: ref('Course') }] },
handler: (req, res) => res.json(courses) },
{ method: 'get', path: '/api/courses/:id', summary: 'Get one course',
responses: { 200: ['The course', ref('Course')], 404: ['Course not found', ref('Problem')] },
handler: (req, res) => res.json(courses.find(c => c.id === Number(req.params.id))) },
{ method: 'post', path: '/api/courses', summary: 'Create a course', auth: true, body: ref('Course'),
responses: { 201: ['Created', ref('Course')], 400: ['Validation failed', ref('Problem')], 401: ['Not signed in', ref('Problem')] },
handler: (req, res) => res.status(201).json(req.body) },
];
function buildSpec(routes) {
const paths = {};
for (const r of routes) {
const parameters = [...r.path.matchAll(/:(\w+)/g)].map(([, name]) => ({ name, in: 'path', required: true, schema: { type: 'integer' } }));
(paths[r.path.replace(/:(\w+)/g, '{$1}')] ??= {})[r.method] = {
summary: r.summary,
...(parameters.length && { parameters }),
...(r.auth && { security: [{ bearerAuth: [] }] }),
...(r.body && { requestBody: { required: true, content: { 'application/json': { schema: r.body } } } }),
responses: Object.fromEntries(Object.entries(r.responses).map(([code, [description, schema]]) =>
[code, { description, content: { 'application/json': { schema } } }])),
};
}
return {
openapi: '3.1.0',
info: { title: 'LearnHub API', version: '1.0.0' },
servers: [{ url: 'https://api.learnhub.example.com' }],
paths,
components: { schemas: { Course, Problem }, securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' } } },
};
}
const app = express();
app.use(express.json());
for (const r of routes) app[r.method](r.path, r.handler);
const spec = buildSpec(routes);
app.get('/openapi.json', (req, res) => res.json(spec));
// Interactive docs: npm install swagger-ui-express, then
// app.use('/docs', swaggerUi.serve, swaggerUi.setup(spec));
const server = app.listen(3115, async () => {
const doc = await (await fetch('http://localhost:3115/openapi.json')).json();
console.log(`OpenAPI ${doc.openapi} — ${doc.info.title} ${doc.info.version}`);
for (const [path, operations] of Object.entries(doc.paths)) {
for (const [method, op] of Object.entries(operations)) {
const lock = op.security ? ' 🔒' : '';
console.log(` ${method.toUpperCase().padEnd(5)} ${path.padEnd(18)} ${op.summary.padEnd(16)} → ${Object.keys(op.responses).join(', ')}${lock}`);
}
}
console.log('Schemas:', Object.keys(doc.components.schemas).join(', '));
server.close();
});
OpenAPI 3.1.0 — LearnHub API 1.0.0
GET /api/courses List courses → 200
POST /api/courses Create a course → 201, 400, 401 🔒
GET /api/courses/{id} Get one course → 200, 404
Schemas: Course, ProblemThis example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- Every route object holds both the handler and its documentation.
- The same array registers Express routes and builds the spec, so they cannot drift apart.
- :id becomes {id} and a path parameter; auth: true adds the bearerAuth security requirement.
- Responses reference shared schemas in components (Course, Problem).
- The spec is served at /openapi.json for Swagger UI, Postman imports and client generators.
Real-world use case#
LearnHub’s mobile team generates a typed Kotlin client from /openapi.json in CI. When an API change breaks the contract, the generated client no longer compiles — before any user sees the bug.
Common mistakes#
A wiki page written once is wrong a month later. Generate or validate the spec in code and CI.
Client developers spend most time on errors. Document 4xx responses and their shape.
Admin-only endpoints in public Swagger UI help attackers map your API. Protect or split the docs.
Best practices#
- OpenAPI 3.1 at /openapi.json
- Reusable schemas and a security scheme
- Generate from code or validate against the spec
- Document every error response
Remember this#
OpenAPI = paths + operations + components.
:id in Express → {id} in OpenAPI.
One source of truth for routes and docs.
Practice exercise#
Your task: Write toOpenApiPath(path) that converts Express paths such as /api/courses/:courseId/lessons/:lessonId into /api/courses/{courseId}/lessons/{lessonId}, and list the parameter names.
Need a hint?
Show solution
const toOpenApiPath = path => path.replace(/:(\w+)/g, '{$1}');
const pathParams = path => [...path.matchAll(/:(\w+)/g)].map(match => match[1]);
const path = '/api/courses/:courseId/lessons/:lessonId';
console.log(toOpenApiPath(path)); // /api/courses/{courseId}/lessons/{lessonId}
console.log(pathParams(path)); // [ 'courseId', 'lessonId' ]
Mini quiz#
Where are reusable schemas defined in an OpenAPI document?
Want more? Practise all Node.js REST API MCQs · Node.js REST API interview questions
My notes#
Saved in this browser.