Skip to main content

OpenAPI 3.1 Spec Guide

OpenAPI 3.1 is the industry standard for describing REST APIs. This guide walks through every section of a production-quality spec — from info and paths to authentication, schema reuse, and automated validation in CI.

✓ Full spec structure ✓ Auth & schema reuse ✓ Swagger UI generation ✓ Spectral validation

Spec Skeleton (OpenAPI 3.1)

openapi: "3.1.0"

info:
  title: Acme API
  version: "1.0.0"
  description: |
    Public REST API for Acme services.
    Rate limits: 100 req/min (standard), 1000 req/min (pro).
  contact:
    name: Acme Engineering
    email: [email protected]
  license:
    name: MIT
    identifier: MIT   # SPDX identifier (new in 3.1)

servers:
  - url: https://api.acme.example/v1
    description: Production
  - url: https://staging-api.acme.example/v1
    description: Staging

tags:
  - name: users
    description: User management
  - name: orders
    description: Order operations

paths:
  /users:
    get:
      operationId: listUsers
      tags: [users]
      summary: List all users
      parameters:
        - $ref: '#/components/parameters/LimitParam'
        - $ref: '#/components/parameters/CursorParam'
      responses:
        '200':
          description: Paginated user list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
      security:
        - BearerAuth: []

    post:
      operationId: createUser
      tags: [users]
      summary: Create a new user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserRequest'
            example:
              email: [email protected]
              name: Jane Doe
      responses:
        '201':
          description: User created
          headers:
            Location:
              description: URL of the created user
              schema:
                type: string
                format: uri
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '422':
          $ref: '#/components/responses/ValidationError'
      security:
        - BearerAuth: []

Components: Schemas, Parameters & Responses

components:
  # --- Reusable schemas ---
  schemas:
    User:
      type: object
      required: [id, email, created_at]
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
          example: "550e8400-e29b-41d4-a716-446655440000"
        email:
          type: string
          format: email
          example: "[email protected]"
        name:
          type: ["string", "null"]  # nullable in OAS 3.1 (not nullable: true)
          example: "Jane Doe"
        roles:
          type: array
          items:
            type: string
            enum: [admin, editor, viewer]
          default: [viewer]
        created_at:
          type: string
          format: date-time
          readOnly: true

    CreateUserRequest:
      type: object
      required: [email]
      properties:
        email:
          type: string
          format: email
        name:
          type: string
          minLength: 1
          maxLength: 100

    UserListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/User'
        meta:
          type: object
          properties:
            next_cursor:
              type: ["string", "null"]
            has_more:
              type: boolean

    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              example: VALIDATION_FAILED
            message:
              type: string
            details:
              type: array
              items:
                type: object
                properties:
                  field:
                    type: string
                  issue:
                    type: string
            trace_id:
              type: string

  # --- Reusable parameters ---
  parameters:
    LimitParam:
      name: limit
      in: query
      description: Number of results per page (max 100)
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20

    CursorParam:
      name: cursor
      in: query
      description: Pagination cursor from previous response
      schema:
        type: string

  # --- Reusable responses ---
  responses:
    Unauthorized:
      description: Missing or invalid authentication token
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

    ValidationError:
      description: Request validation failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

  # --- Security schemes ---
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key

    OAuth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://auth.acme.example/oauth/authorize
          tokenUrl: https://auth.acme.example/oauth/token
          scopes:
            read:users: Read user data
            write:users: Create and update users

Validation with Spectral (CI)

# Install Spectral CLI
npm install -g @stoplight/spectral-cli

# Lint against built-in OAS ruleset
spectral lint openapi.yaml

# Custom ruleset (.spectral.yaml)
extends: ["spectral:oas"]
rules:
  operation-tag-defined: error       # all operations must have tags
  operation-success-response: error  # must have 2xx response
  no-$ref-siblings: off              # relax for OAS 3.1
  info-contact: warn
  oas3-api-servers: error

# GitHub Actions integration
# .github/workflows/api-lint.yml
name: Lint OpenAPI Spec
on: [pull_request]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm install -g @stoplight/spectral-cli
      - run: spectral lint openapi.yaml --fail-severity=warn

Generating Swagger UI & Redoc

// Express + swagger-ui-express
import express from 'express';
import swaggerUi from 'swagger-ui-express';
import yaml from 'js-yaml';
import fs from 'fs';

const app = express();
const spec = yaml.load(fs.readFileSync('./openapi.yaml', 'utf8'));

app.use('/docs', swaggerUi.serve, swaggerUi.setup(spec, {
  customCss: '.swagger-ui .topbar { display: none }',
  swaggerOptions: { persistAuthorization: true },
}));

// Serve raw spec for SDK generators
app.get('/openapi.json', (req, res) => res.json(spec));
# Generate static Redoc HTML (zero runtime deps)
npx @redocly/cli build-docs openapi.yaml -o docs/api.html

# Bundle multi-file spec into single file
npx @redocly/cli bundle openapi.yaml -o dist/openapi.yaml

# Generate TypeScript client SDK
npx @openapitools/openapi-generator-cli generate \
  -i openapi.yaml \
  -g typescript-fetch \
  -o src/api-client

Tooling Quick Reference

Tool Purpose Command
SpectralLint & custom rulesspectral lint openapi.yaml
Redocly CLILint, bundle, build docsredocly lint / bundle / build-docs
vacuumFast Go-based lintervacuum lint openapi.yaml
openapi-generatorGenerate client SDKsopenapi-generator-cli generate
swagger-ui-expressServe interactive docsnpm i swagger-ui-express
PrismMock server from specprism mock openapi.yaml

Frequently Asked Questions

What is the difference between OpenAPI 3.0 and 3.1?▼

OpenAPI 3.1 (released 2021) aligns fully with JSON Schema Draft 2020-12, whereas 3.0 used a subset of JSON Schema Draft 4. Key improvements in 3.1: webhooks are a first-class concept (separate from paths), the info.license object now supports SPDX expressions, $ref can now appear alongside other properties (3.0 forbade sibling keywords), the nullable: true hack is replaced by type: ['string', 'null'] following JSON Schema conventions, and $vocabulary support enables custom validation keywords. Use 3.1 for all new APIs — tooling support caught up in 2022-2023.

How do I define authentication in an OpenAPI spec?▼

Authentication is defined in two places. First, declare security schemes under components.securitySchemes: for Bearer JWT use type: http, scheme: bearer, bearerFormat: JWT. For API keys use type: apiKey with in: header or in: query and the name field. For OAuth2 use type: oauth2 with flows describing authorization, implicit, clientCredentials, or password flows. Second, apply them: global security array sets defaults for all operations, individual operations can override with their own security array, or opt out with security: [] for public endpoints.

What tools validate an OpenAPI spec?▼

Spectral by Stoplight is the most powerful validator — it has built-in OAS3 ruleset and you can write custom rules as JS/YAML rulesets. Install with npm i -g @stoplight/spectral-cli then run spectral lint openapi.yaml. Other options: swagger-parser (programmatic validation in Node.js), vacuum (Go-based, extremely fast, CI-friendly), redocly CLI (redocly lint, also has its own ruleset), and the official swagger-editor online validator at editor.swagger.io. For CI, run Spectral or redocly in your GitHub Actions workflow to block PRs with invalid specs.

Should I write OpenAPI by hand or generate it from code?▼

Code-first generation (annotations or decorators on route handlers) keeps the spec in sync automatically but produces verbose, hard-to-read specs and couples your API contract to implementation details. Spec-first (design the YAML manually, then generate server stubs and client SDKs) enforces deliberate API design and makes contract changes explicit and reviewable. Best practice: write the spec first as a design artifact, review it with stakeholders, then generate server stubs (openapi-generator, oapi-codegen for Go, FastAPI with Pydantic models). This makes breaking changes visible in diffs.

How do I generate a Swagger UI from my OpenAPI spec?▼

The simplest way is the swagger-ui-express package: npm i swagger-ui-express js-yaml, then app.use('/docs', swaggerUi.serve, swaggerUi.setup(yaml.load(fs.readFileSync('./openapi.yaml')))). For static hosting, run npx @redocly/cli build-docs openapi.yaml -o docs/index.html to produce a zero-dependency HTML file. Redoc produces cleaner documentation than default Swagger UI and handles complex schemas better. For Next.js or Astro projects, serve the raw openapi.yaml and initialize swagger-ui-dist client-side.

What is the best way to handle $ref and schema reuse?▼

Define all reusable schemas under components.schemas, parameters under components.parameters, and responses under components.responses. Use $ref: '#/components/schemas/User' to reference them. For request/response bodies, define them inline for simple one-off cases and in components for any schema used more than once. For large APIs, split into multiple files and use $ref: './schemas/user.yaml'. Tools like Redocly and swagger-parser handle multi-file specs with bundle commands that merge everything into a single file for distribution.

Share this tool: X / Twitter LinkedIn
✓ 60,000+ developers use DevPlaybook ★★★★★ 4.8 / 5 on Gumroad ✓ MIT licensed · Instant download ✓ No login required

Related Products

on Gumroad

You Might Also Need

More Free Tools

Enjoying this tool? Get more like it.

Weekly developer tool guides, productivity playbooks & AI tricks. Free. No spam.

DevPlaybook Pro 7 days free

Unlock AI-Powered Dev Tools

  • ⚡ AI Code Review, Doc Generator & SQL Builder
  • ⚡ All premium templates & early access
  • ⚡ Member discounts on Gumroad products
Start Free Trial →

$9/mo after trial · Cancel anytime

Want the full toolkit?

Get DevPlaybook Pro

Every template, guide, boilerplate, and automation script in one bundle. 13 premium products — grab them all at once and save big.

$79
$241 separately Save $162 (67% off)
Get DevPlaybook Pro — $79 →

MIT licensed · Instant download · No subscription

See what's included → Browse all deals →