API Design

OpenAPI Specification

A machine-readable, language-agnostic standard for describing RESTful APIs — OpenAPI 3.1 document structure, JSON Schema alignment, tooling ecosystem (Swagger UI, Redoc, code generators), API-first design workflow, and contract testing with Schemathesis and Dredd.

⏱ 13 min read

What it is

OpenAPI (formerly Swagger) is a vendor-neutral specification for describing HTTP APIs in a structured YAML or JSON document. An OpenAPI document captures the full interface contract: available endpoints, HTTP methods, request parameters and bodies, response schemas, authentication schemes, and metadata. OpenAPI 3.1 (released 2021) aligns fully with JSON Schema Draft 2020-12, eliminating the previous subset incompatibilities of OAS 3.0.

The specification is maintained by the OpenAPI Initiative (a Linux Foundation project) and has become the de facto standard for documenting REST APIs, enabling an ecosystem of tools for documentation, mocking, SDK generation, and contract testing.

Why it exists

Without a formal contract, the only documentation of an API is its implementation — subject to change, often stale prose, and requiring consumers to reverse-engineer behaviour. OpenAPI creates a single source of truth: documentation, mocks, generated SDKs, and contract tests can all derive from the same document. This accelerates parallel development (frontend and backend teams can work against a mock before the real implementation is ready) and catches breaking changes before they reach production.

Document structure

openapi: "3.1.0"
info:
  title: Orders API
  version: "2.1.0"
  description: Manage customer orders.
  contact:
    name: Platform Team
    email: platform@example.com

servers:
  - url: https://api.example.com/v2
    description: Production
  - url: https://api.staging.example.com/v2
    description: Staging

security:
  - bearerAuth: []          # Default security for all paths

paths:
  /orders:
    get:
      operationId: listOrders
      summary: List orders for the authenticated user
      tags: [Orders]
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, confirmed, shipped, delivered]
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        "200":
          description: Paginated list of orders
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrderPage"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /orders/{orderId}:
    get:
      operationId: getOrder
      tags: [Orders]
      parameters:
        - name: orderId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
        "404":
          $ref: "#/components/responses/NotFound"

components:
  schemas:
    Order:
      type: object
      required: [id, status, total, createdAt]
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        status:
          type: string
          enum: [pending, confirmed, shipped, delivered, cancelled]
        total:
          type: number
          format: decimal
          minimum: 0
        createdAt:
          type: string
          format: date-time
          readOnly: true
        items:
          type: array
          items:
            $ref: "#/components/schemas/OrderItem"
    OrderPage:
      type: object
      required: [data, pagination]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Order"
        pagination:
          $ref: "#/components/schemas/Pagination"
    Pagination:
      type: object
      properties:
        nextCursor:
          type: string
          nullable: true
        hasMore:
          type: boolean

  responses:
    Unauthorized:
      description: Authentication is required
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
    NotFound:
      description: Resource not found
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

API-first design workflow

In API-first development, the OpenAPI document is authored before implementation begins. The workflow is:

  1. Design — write the OpenAPI document collaboratively (Stoplight Studio, Swagger Editor); review in a PR.
  2. Mock — stand up a mock server from the spec (Prism, Stoplight Prism, WireMock with contract). Frontend teams develop against the mock immediately.
  3. Implement — generate server stubs or use the spec to drive TDD. The spec is the contract, not the implementation.
  4. Validate — run contract tests against the real implementation to verify it matches the spec.
  5. Publish — serve Swagger UI or Redoc documentation from the live spec. Lint the spec in CI with Spectral.

Tooling ecosystem

  • Swagger UI / Redoc — render interactive HTML documentation from an OpenAPI document. Swagger UI lets users try endpoints inline; Redoc produces a three-panel reference layout.
  • Prism — a mock server that generates example responses from the spec; validates requests against the schema in proxy mode.
  • OpenAPI Generator / Kiota — generate typed client SDKs (TypeScript, Java, Python, Go, C#, and 50+ more) and server stubs from the spec.
  • Spectral — a linter for OpenAPI documents; enforces API style guides (naming conventions, required fields, security schemes); integrates into CI.
  • Schemathesis — property-based contract testing tool that generates hundreds of test cases from the OpenAPI spec and fuzzes the live API for edge cases, schema violations, and server errors.
  • Dredd — transaction-level contract testing; runs requests from the spec against the real server and asserts responses match the documented schemas.

Pros and cons

Pros

  • Single source of truth for documentation, mocks, SDKs, and contract tests.
  • Enables parallel frontend/backend development via mock servers before implementation.
  • Generated SDKs eliminate hand-written client code and keep types in sync with the server.
  • Spectral linting enforces API design standards across teams automatically.
  • OAS 3.1 full JSON Schema alignment removes the frustrating subset incompatibilities of 3.0.

Cons

  • Code-first approaches often produce low-quality specs from framework annotations — verbose, missing descriptions, poor examples.
  • Large OpenAPI documents become hard to maintain without a $ref decomposition strategy and tooling.
  • OpenAPI doesn't capture behavioural semantics (ordering guarantees, rate limits, side effects) — only structural contracts.
  • Generated SDKs may need manual customisation for retry logic, authentication, and pagination.
  • Webhook and async API patterns are awkward in OpenAPI 3.1; AsyncAPI is a better fit for event-driven APIs.

Implementation notes

Prefer $ref decomposition

For any non-trivial API, keep schemas in components/schemas and reference them with $ref rather than inlining. Split large specs across files (paths/orders.yaml, schemas/order.yaml) and bundle them for distribution with redocly bundle or the Swagger CLI. This keeps individual files reviewable in PRs and avoids a single 5,000-line YAML file.

Write example objects

Every schema should include at least one example value. Prism uses examples to generate mock responses; Redoc and Swagger UI display them to developers. Use components/examples for named, reusable examples that can be referenced from multiple operations. Provide examples that cover the full object shape — not just required fields.

Schemathesis contract testing

Schemathesis is a property-based testing tool that reads the OpenAPI spec and automatically generates hundreds of test cases by varying parameter values, header values, and request bodies. It checks that responses match the documented status codes and schemas, and that the server never returns a 500. Run it as part of CI against a staging environment. Schemathesis catches bugs that hand-written test cases miss (missing null handling, unexpected enum values, invalid content types).

Documenting security requirements

Define security schemes in components/securitySchemes and apply them globally (top-level security field) with per-operation overrides for public endpoints. Document required OAuth2 scopes per operation using the scopes field in the security requirement. This allows API gateway tooling to auto-configure scope enforcement from the spec.

Decision checklist

  • Is the OpenAPI spec authored before implementation (API-first), not extracted after?
  • Does every operation have a unique operationId?
  • Are all schemas in components/schemas with $ref references?
  • Does every schema have at least one example?
  • Is Spectral linting running in CI against the spec?
  • Are Schemathesis or Dredd contract tests running against staging on every PR?
  • Are security schemes and required scopes documented per endpoint?
  • Are error responses documented with application/problem+json (RFC 9457)?
  • REST API Design — OpenAPI is the standard contract format for REST APIs.
  • API Versioning — document versioning strategy in the info.version and servers fields.
  • Contract Testing — Schemathesis and Dredd use OpenAPI documents as the contract source.
  • API Authorization — document OAuth2 scopes and security schemes in the OpenAPI spec.

Further reading

  • OpenAPI Specification 3.1 — spec.openapis.org/oas/v3.1.0
  • Swagger Editor — editor.swagger.io
  • Spectral OpenAPI Linting — stoplight.io/open-source/spectral
  • Schemathesis documentation — schemathesis.readthedocs.io
  • AsyncAPI (for event-driven APIs) — asyncapi.com