Back to Technology

API Development Series Part 3: OpenAPI Specification

January 31, 2026 Wasil Zafar 40 min read

Master OpenAPI Specification 3.0/3.1 including contract-first API design, paths, operations, schemas, oneOf/allOf/anyOf modeling, JSON Schema alignment, and design-first workflow.

Table of Contents

  1. Why OpenAPI Matters
  2. Paths & Operations
  3. Parameters & Request Body
  4. Components & Schemas
  5. oneOf/allOf/anyOf
  6. Contract-First Workflow
Series Navigation: This is Part 3 of the 17-part API Development Series. Review Part 2: Data Persistence first.

Why OpenAPI Matters

The API Contract Problem

Without a formal API specification, teams face constant challenges: miscommunication between frontend and backend developers, outdated documentation, inconsistent error handling, and broken integrations. OpenAPI (formerly Swagger) solves this by providing a machine-readable contract that all parties can trust.

Contract-First Development: Define your API specification before writing code. This approach catches design issues early, enables parallel development, and ensures documentation is always accurate.

Benefits of OpenAPI

  • Single Source of Truth: One specification drives docs, mocks, and validation
  • Code Generation: Generate client SDKs, server stubs, and test cases
  • Interactive Documentation: Swagger UI lets developers try endpoints instantly
  • Validation: Automatically validate requests and responses against the spec
  • API Governance: Enforce design standards with linting tools like Spectral

OpenAPI 3.0 vs 3.1

Feature OpenAPI 3.0 OpenAPI 3.1
JSON Schema Subset (incompatible) Full Draft 2020-12 alignment
Null support nullable: true type: ['string', 'null']
Webhooks Not supported Native webhook definitions
License Apache 2.0 Apache 2.0 + SPDX identifiers

Your First OpenAPI Document

# openapi.yaml - Basic structure
openapi: 3.1.0
info:
  title: Task Management API
  version: 1.0.0
  description: |
    A RESTful API for managing tasks and projects.
    
    ## Features
    - Create, read, update, delete tasks
    - Organize tasks into projects
    - Filter and search tasks
  contact:
    name: API Support
    email: api-support@example.com
    url: https://developer.example.com
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT

servers:
  - url: https://api.example.com/v1
    description: Production server
  - url: https://staging-api.example.com/v1
    description: Staging server
  - url: http://localhost:3000/v1
    description: Local development

tags:
  - name: Tasks
    description: Task management operations
  - name: Projects
    description: Project management operations

paths:
  # Define your endpoints here
  
components:
  # Reusable schemas, security schemes, etc.

Paths & Operations

Defining Endpoints

Paths define the available endpoints in your API. Each path can support multiple HTTP methods (operations).

paths:
  /tasks:
    get:
      tags:
        - Tasks
      summary: List all tasks
      description: Returns a paginated list of tasks with optional filtering
      operationId: listTasks
      parameters:
        - name: status
          in: query
          description: Filter by task status
          schema:
            type: string
            enum: [pending, in_progress, completed]
        - name: limit
          in: query
          description: Maximum number of results
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: cursor
          in: query
          description: Pagination cursor from previous response
          schema:
            type: string
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
    
    post:
      tags:
        - Tasks
      summary: Create a new task
      description: Creates a new task and returns the created resource
      operationId: createTask
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTaskRequest'
            examples:
              simple:
                summary: Simple task
                value:
                  title: "Review PR #123"
                  status: pending
              detailed:
                summary: Detailed task
                value:
                  title: "Implement user authentication"
                  description: "Add JWT-based auth with refresh tokens"
                  status: pending
                  priority: high
                  due_date: "2024-02-15"
                  project_id: "proj_abc123"
      responses:
        '201':
          description: Task created successfully
          headers:
            Location:
              description: URL of the created task
              schema:
                type: string
                format: uri
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '400':
          $ref: '#/components/responses/BadRequest'
        '422':
          $ref: '#/components/responses/ValidationError'

Path Parameters

  /tasks/{taskId}:
    parameters:
      - name: taskId
        in: path
        required: true
        description: Unique task identifier
        schema:
          type: string
          pattern: '^task_[a-zA-Z0-9]{8,}$'
          example: task_abc12345
    
    get:
      tags:
        - Tasks
      summary: Get a specific task
      operationId: getTask
      responses:
        '200':
          description: Task found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '404':
          $ref: '#/components/responses/NotFound'
    
    patch:
      tags:
        - Tasks
      summary: Update a task
      operationId: updateTask
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTaskRequest'
      responses:
        '200':
          description: Task updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
    
    delete:
      tags:
        - Tasks
      summary: Delete a task
      operationId: deleteTask
      responses:
        '204':
          description: Task deleted successfully
        '404':
          $ref: '#/components/responses/NotFound'

Parameters & Request Body

Parameter Locations

Parameters can appear in four locations: path, query, header, and cookie.

parameters:
  # Path parameter (in URL)
  - name: taskId
    in: path
    required: true
    schema:
      type: string
  
  # Query parameter (?key=value)
  - name: status
    in: query
    required: false
    schema:
      type: string
      enum: [pending, completed]
  
  # Header parameter
  - name: X-Request-ID
    in: header
    required: false
    description: Unique request identifier for tracing
    schema:
      type: string
      format: uuid
  
  # Cookie parameter
  - name: session_id
    in: cookie
    required: false
    schema:
      type: string

Reusable Parameters

components:
  parameters:
    # Pagination parameters
    LimitParam:
      name: limit
      in: query
      description: Maximum number of items to return
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
    
    CursorParam:
      name: cursor
      in: query
      description: Cursor for pagination
      schema:
        type: string
    
    # Common filters
    StatusFilter:
      name: status
      in: query
      description: Filter by status
      schema:
        type: string
        enum: [active, inactive, archived]
    
    # Sorting
    SortParam:
      name: sort
      in: query
      description: Sort field and direction
      schema:
        type: string
        pattern: '^-?[a-z_]+$'
        example: '-created_at'

# Usage in paths
paths:
  /tasks:
    get:
      parameters:
        - $ref: '#/components/parameters/LimitParam'
        - $ref: '#/components/parameters/CursorParam'
        - $ref: '#/components/parameters/StatusFilter'

Request Body Schemas

requestBody:
  required: true
  content:
    application/json:
      schema:
        type: object
        required:
          - title
        properties:
          title:
            type: string
            minLength: 1
            maxLength: 200
            description: Task title
          description:
            type: string
            maxLength: 2000
            description: Detailed task description
          status:
            type: string
            enum: [pending, in_progress, completed]
            default: pending
          priority:
            type: string
            enum: [low, medium, high, urgent]
            default: medium
          due_date:
            type: string
            format: date
            description: Due date in ISO 8601 format
          assignee_id:
            type: string
            description: User ID of the assignee
          tags:
            type: array
            items:
              type: string
            maxItems: 10
            description: Labels for categorization
Validation Best Practice: Always define required, minLength, maxLength, minimum, maximum, and pattern constraints. These enable automatic request validation.

Components & Schemas

Defining Reusable Schemas

The components/schemas section contains reusable data models. Well-designed schemas reduce duplication and ensure consistency.

components:
  schemas:
    # Base entity with common fields
    BaseEntity:
      type: object
      properties:
        id:
          type: string
          readOnly: true
          description: Unique identifier
          example: task_abc12345
        created_at:
          type: string
          format: date-time
          readOnly: true
          description: Creation timestamp
        updated_at:
          type: string
          format: date-time
          readOnly: true
          description: Last update timestamp
    
    # Task schema
    Task:
      allOf:
        - $ref: '#/components/schemas/BaseEntity'
        - type: object
          required:
            - title
            - status
          properties:
            title:
              type: string
              minLength: 1
              maxLength: 200
            description:
              type: string
              maxLength: 2000
            status:
              $ref: '#/components/schemas/TaskStatus'
            priority:
              $ref: '#/components/schemas/Priority'
            due_date:
              type: string
              format: date
            assignee:
              $ref: '#/components/schemas/UserSummary'
            project:
              $ref: '#/components/schemas/ProjectSummary'
            tags:
              type: array
              items:
                type: string
    
    # Enums as separate schemas for reuse
    TaskStatus:
      type: string
      enum: [pending, in_progress, completed, cancelled]
      description: Current task status
    
    Priority:
      type: string
      enum: [low, medium, high, urgent]
      default: medium
    
    # Embedded/summary objects
    UserSummary:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        avatar_url:
          type: string
          format: uri
    
    ProjectSummary:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        color:
          type: string
          pattern: '^#[0-9A-Fa-f]{6}$'

Request vs Response Schemas

    # Input schema - what clients send
    CreateTaskRequest:
      type: object
      required:
        - title
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
        status:
          $ref: '#/components/schemas/TaskStatus'
        priority:
          $ref: '#/components/schemas/Priority'
        due_date:
          type: string
          format: date
        project_id:
          type: string
        assignee_id:
          type: string
        tags:
          type: array
          items:
            type: string
    
    # Partial update schema
    UpdateTaskRequest:
      type: object
      minProperties: 1  # At least one field required
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
        status:
          $ref: '#/components/schemas/TaskStatus'
        priority:
          $ref: '#/components/schemas/Priority'
        due_date:
          type: ['string', 'null']
          format: date
        assignee_id:
          type: ['string', 'null']
    
    # Collection response with pagination
    TaskList:
      type: object
      required:
        - data
        - pagination
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Task'
        pagination:
          $ref: '#/components/schemas/CursorPagination'
    
    CursorPagination:
      type: object
      properties:
        next_cursor:
          type: ['string', 'null']
          description: Cursor for the next page, null if no more pages
        has_more:
          type: boolean
          description: Whether more results are available

oneOf/allOf/anyOf

Schema Composition

OpenAPI provides three keywords for combining schemas, each with different semantics.

Composition Keywords

Reference
Keyword Meaning Use Case
allOf Must match ALL schemas Inheritance, extending base schemas
oneOf Must match exactly ONE schema Polymorphism, discriminated unions
anyOf Must match at least ONE schema Optional variants, flexible inputs

allOf - Schema Inheritance

# Base schema
BaseEntity:
  type: object
  properties:
    id:
      type: string
      readOnly: true
    created_at:
      type: string
      format: date-time
      readOnly: true

# Extended schema using allOf
Task:
  allOf:
    - $ref: '#/components/schemas/BaseEntity'
    - type: object
      required:
        - title
      properties:
        title:
          type: string
        status:
          type: string

# Result: Task has id, created_at, title, and status

oneOf - Polymorphic Types (Discriminator)

# Notification can be email, sms, or push
Notification:
  oneOf:
    - $ref: '#/components/schemas/EmailNotification'
    - $ref: '#/components/schemas/SmsNotification'
    - $ref: '#/components/schemas/PushNotification'
  discriminator:
    propertyName: type
    mapping:
      email: '#/components/schemas/EmailNotification'
      sms: '#/components/schemas/SmsNotification'
      push: '#/components/schemas/PushNotification'

EmailNotification:
  type: object
  required:
    - type
    - to
    - subject
    - body
  properties:
    type:
      type: string
      enum: [email]
    to:
      type: string
      format: email
    subject:
      type: string
    body:
      type: string
    cc:
      type: array
      items:
        type: string
        format: email

SmsNotification:
  type: object
  required:
    - type
    - phone_number
    - message
  properties:
    type:
      type: string
      enum: [sms]
    phone_number:
      type: string
      pattern: '^\+[1-9]\d{1,14}$'
    message:
      type: string
      maxLength: 160

PushNotification:
  type: object
  required:
    - type
    - device_token
    - title
  properties:
    type:
      type: string
      enum: [push]
    device_token:
      type: string
    title:
      type: string
    body:
      type: string
    data:
      type: object
      additionalProperties: true

anyOf - Flexible Inputs

# User identifier can be ID or email
UserIdentifier:
  anyOf:
    - type: object
      required: [user_id]
      properties:
        user_id:
          type: string
    - type: object
      required: [email]
      properties:
        email:
          type: string
          format: email

# Search query accepts multiple filter types
SearchFilter:
  anyOf:
    - $ref: '#/components/schemas/DateRangeFilter'
    - $ref: '#/components/schemas/StatusFilter'
    - $ref: '#/components/schemas/TextFilter'

Error Responses with RFC 7807

components:
  schemas:
    # RFC 7807 Problem Details
    ProblemDetails:
      type: object
      required:
        - type
        - title
        - status
      properties:
        type:
          type: string
          format: uri
          description: URI identifying the problem type
          example: 'https://api.example.com/errors/validation'
        title:
          type: string
          description: Human-readable summary
          example: 'Validation Error'
        status:
          type: integer
          description: HTTP status code
          example: 422
        detail:
          type: string
          description: Human-readable explanation
          example: 'The request body contains invalid fields'
        instance:
          type: string
          format: uri
          description: URI of the specific occurrence
    
    ValidationProblem:
      allOf:
        - $ref: '#/components/schemas/ProblemDetails'
        - type: object
          properties:
            errors:
              type: array
              items:
                type: object
                properties:
                  field:
                    type: string
                  message:
                    type: string
                  code:
                    type: string
  
  responses:
    BadRequest:
      description: Bad request
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
    
    Unauthorized:
      description: Authentication required
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
    
    NotFound:
      description: Resource not found
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
    
    ValidationError:
      description: Validation failed
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ValidationProblem'

Contract-First Workflow

Design-First Development

In contract-first development, you design the API specification before writing any implementation code. This approach has several advantages:

  1. Early Feedback: Stakeholders can review the API design before coding begins
  2. Parallel Development: Frontend and backend teams can work simultaneously
  3. Better Design: Thinking about the interface first often leads to cleaner APIs
  4. Accurate Documentation: Docs are always in sync because they're the source of truth

Contract-First Workflow

Process
  1. Design: Write OpenAPI spec collaboratively
  2. Review: Get stakeholder approval on the interface
  3. Mock: Generate mock server for frontend development
  4. Implement: Build backend against the specification
  5. Validate: Test implementation matches specification
  6. Document: Generate docs from the same spec

Tools for Contract-First Development

Spectral - API Linting

# .spectral.yaml - Custom ruleset
extends: spectral:oas
rules:
  # Require descriptions on all operations
  operation-description:
    description: Operations must have descriptions
    given: "$.paths[*][*]"
    then:
      field: description
      function: truthy
  
  # Enforce consistent naming
  path-keys-kebab-case:
    description: Path segments should use kebab-case
    given: "$.paths[*]~"
    then:
      function: pattern
      functionOptions:
        match: "^(/[a-z][a-z0-9-]*)+$"
  
  # Require examples
  schema-examples:
    description: Schemas should have examples
    given: "$.components.schemas[*]"
    then:
      field: example
      function: truthy
# Run Spectral linting
npx @stoplight/spectral-cli lint openapi.yaml

# Output:
# ✖ 2 problems (1 error, 1 warning)
# openapi.yaml
#   23:5  error    operation-description  Operations must have descriptions
#   45:3  warning  schema-examples       Schemas should have examples

Prism - Mock Server

# Start mock server from spec
npx @stoplight/prism-cli mock openapi.yaml

# Prism is running at http://127.0.0.1:4010
# Now you can make requests:
curl http://localhost:4010/tasks

# Prism returns example responses from the spec

OpenAPI Generator - Code Generation

# Generate TypeScript client
npx @openapitools/openapi-generator-cli generate \
  -i openapi.yaml \
  -g typescript-axios \
  -o ./generated/client

# Generate Node.js server stub
npx @openapitools/openapi-generator-cli generate \
  -i openapi.yaml \
  -g nodejs-express-server \
  -o ./generated/server

# Generate Python FastAPI server
npx @openapitools/openapi-generator-cli generate \
  -i openapi.yaml \
  -g python-fastapi \
  -o ./generated/python-server

Validating Implementation Against Spec

// Using express-openapi-validator
const OpenApiValidator = require('express-openapi-validator');

app.use(
  OpenApiValidator.middleware({
    apiSpec: './openapi.yaml',
    validateRequests: true,
    validateResponses: true, // Also validate your responses!
    validateSecurity: {
      handlers: {
        BearerAuth: (req) => {
          // Validate JWT token
          return validateJwt(req.headers.authorization);
        }
      }
    }
  })
);

// All requests are now validated against the OpenAPI spec
// Invalid requests return 400 with validation errors
// Invalid responses return 500 (caught in development)

Practice Exercises

Exercise 1: Define a Blog API

Beginner 30 minutes

Write an OpenAPI specification for a blog API:

  • Resources: Posts, Comments, Authors
  • CRUD operations for posts
  • Nested route: /posts/{id}/comments
  • Pagination on list endpoints
  • Proper error responses

Exercise 2: Add Polymorphic Types

Intermediate 45 minutes

Extend your blog API with:

  • Content blocks using oneOf (text, image, code, quote)
  • Media attachments with discriminator
  • Rich examples for each type

Exercise 3: Set Up Contract-First Workflow

Advanced 60 minutes

Create a complete contract-first setup:

  • Write custom Spectral rules for your organization
  • Set up Prism mock server
  • Generate TypeScript client SDK
  • Add request/response validation middleware
  • Create GitHub Actions workflow for spec validation
Next Steps: In Part 4: Documentation & Developer Experience, we'll transform your OpenAPI specification into beautiful, interactive documentation using Swagger UI, Redocly, and developer portal design.
Technology