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.

ASOasis
8 min read
OpenAPI and Swagger: A Practical Tutorial to Document Your REST API

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.

  1. Install dependencies:
npm i express swagger-ui-express js-yaml
  1. 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'));
  1. 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