API Documentation Is Not an Afterthought. It Is Part of the Design.

by Eric Hanson, Backend Developer at Clean Systems Consulting

When documentation reveals design problems

The clearest sign that an API design needs work is when its documentation requires extensive explanation. If a single endpoint needs three paragraphs of caveats — "but only if the account type is X," "unless the flag was set during creation," "note that this field means something different for batch requests" — that is a design problem surfaced through documentation.

Writing documentation before you build is design work. It forces you to explain the API from the outside in, as a developer who has no access to your implementation. Every awkward sentence, every edge case you find hard to explain, every conditional behavior you have to caveat — these are signals about the design itself.

The practice is not new: it parallels test-driven development. Write the spec first. If the spec is hard to write, the implementation will be hard to use.

OpenAPI as the contract

OpenAPI (formerly Swagger) is the standard format for describing REST APIs. An OpenAPI spec is machine-readable, can generate client SDKs in dozens of languages, powers interactive documentation tools (Swagger UI, Redoc, Scalar), and can be used for contract testing.

A minimal but complete OpenAPI spec for a single endpoint:

openapi: 3.1.0
info:
  title: Orders API
  version: 1.0.0

paths:
  /orders/{id}:
    get:
      summary: Retrieve an order by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            pattern: '^[0-9A-Z]{26}$'
          description: ULID of the order
      responses:
        '200':
          description: Order found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '404':
          description: Order not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'

The pattern ^[0-9A-Z]{26}$ in the parameter schema tells the developer what a valid ID looks like without them having to try invalid values and debug 404s. The 404 response schema tells them exactly what comes back when an order is not found. Both of these are documentation decisions that prevent support tickets.

What the spec cannot do alone

OpenAPI describes structure. It does not describe intent.

A field named status with type string is structurally documented. But a developer needs to know: what values are possible? Are they stable or can they expand? Does the transition between states follow a defined state machine? What operations are available at each status?

This context belongs in the description field of your schema — not in a separate wiki page, not in a comment in someone's Slack message:

components:
  schemas:
    Order:
      properties:
        status:
          type: string
          enum: [pending, confirmed, shipped, delivered, cancelled]
          description: |
            Current state of the order.
            - pending: payment received, not yet fulfilled
            - confirmed: warehouse has acknowledged the order
            - shipped: tracking number issued
            - delivered: confirmed by carrier
            - cancelled: order voided; see cancellation_reason
            
            New status values may be added in future API versions.
            Clients must handle unknown status values gracefully.

The last line — "clients must handle unknown status values gracefully" — is a versioning policy embedded in the documentation. A developer reading this knows to build a default case into their status handling logic.

Examples are not optional

Abstract descriptions and schema definitions tell developers what is allowed. Examples tell developers what is typical. Both are necessary.

Every request body and response schema should have at least one example. For complex endpoints, include multiple examples covering the main cases:

content:
  application/json:
    examples:
      physical_product:
        summary: Order for a physical product
        value:
          id: "01HZQK7P3WVXBN4Y9MRDTJC8E6"
          status: "shipped"
          tracking_number: "1Z999AA10123456784"
      digital_product:
        summary: Order for a digital download
        value:
          id: "01HZQKBRW4X9L3F7G2ESHYD0M8"
          status: "delivered"
          download_url: "https://cdn.example.com/files/..."

A developer reading these examples understands that physical orders have tracking numbers and digital orders have download URLs, without you having to explicitly document the conditional logic.

The changelog is part of the documentation

Every API change — breaking or non-breaking — should be in a changelog. Not just "bug fixes and improvements." The changelog is the document a developer reads when their integration breaks:

## 2026-04-19

### Added
- `GET /orders` now supports `status` filter parameter (e.g., `?status=shipped`)
- Orders now include `estimated_delivery` field (nullable, present when order is shipped)

### Changed
- `POST /orders` now validates that `shipping.postal_code` matches the format
  for the specified `shipping.country`. Previously accepted any string.
  (This is a behavioral change; the request schema is unchanged.)

### Deprecated
- `shipping_address` string field is deprecated in favor of `shipping` object.
  Will be removed in v3. See migration guide.

Behavioral changes — like the postal code validation tightening — are especially important to document. They are not schema changes, so schema diffing tools will not catch them. The changelog is the only place they live.

Keep the spec close to the code

Documentation that lives in a separate repository or wiki falls out of sync. The OpenAPI spec should live in the same repository as the API code and be validated in CI:

npx @redocly/cli lint openapi.yaml

Better: generate the spec from code annotations and export it as an artifact on every build. The spec is always current because it is derived from the running code. Tools like springdoc-openapi (Java/Spring), drf-spectacular (Django), and FastAPI's built-in generation support this pattern.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

Why Austin Startups Are Rethinking Local-Only Backend Hiring

The case for keeping your backend team local used to be obvious. The math has changed, and a lot of founders are noticing.

Read more

Broken Object-Level Authorization in Spring Boot — How to Detect and Prevent IDOR

IDOR (Insecure Direct Object Reference) is consistently the most common API vulnerability. It occurs when an API endpoint accepts a resource identifier and returns or modifies the resource without verifying the caller has permission to access that specific resource.

Read more

Why Architecture Decisions Matter More Than Frameworks

Why do some apps crash after a minor update while others scale effortlessly? Often, it’s not the fancy framework—they’re just tools. The real magic (or disaster) starts with architecture.

Read more

From Figma to Database: The Hidden Complexity of Backend Development

Turning a Figma design into a working system sounds simple—until you reach the backend. Behind every screen lies layers of logic, data, and decisions most people never see.

Read more