Node.js REST API · Delivery

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.

Advanced 7 min Lesson 19 of 25
Before you start:Request Validation
0/25 in Node.js REST API
On this page · 7 min · Advanced

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#

YAML Illustrative
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#

JavaScriptopenapi.js Illustrative
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();
});
Expected output
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, Problem

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. Every route object holds both the handler and its documentation.
  2. The same array registers Express routes and builds the spec, so they cannot drift apart.
  3. :id becomes {id} and a path parameter; auth: true adds the bearerAuth security requirement.
  4. Responses reference shared schemas in components (Course, Problem).
  5. 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#

Hand-written docs that drift

A wiki page written once is wrong a month later. Generate or validate the spec in code and CI.

Documenting only 200 responses

Client developers spend most time on errors. Document 4xx responses and their shape.

Exposing internal docs publicly

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?
path.replace(/:(\w+)/g, "{$1}") and [...path.matchAll(/:(\w+)/g)].
Show solution
JavaScript Illustrative
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#

API Documentation quizMultiple choice Question 1 of 3

Where are reusable schemas defined in an OpenAPI document?

Score so far: 0/0

Want more? Practise all Node.js REST API MCQs · Node.js REST API interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.