Aroma Store's v1 contract is complete: resources, URIs, methods, codes, representations, collections and a versioning policy. But a contract that only exists in the heads of the team that designed it is not a contract: it is a tacit agreement waiting to be broken. This lesson covers the piece that turns the design into something other people can use. We will see why documentation is part of the product and not an extra, which kinds of document serve which reader, what each endpoint's reference must include, and what changes radically when the contract is written in a machine-readable format such as OpenAPI. We close module 2 by taking stock of the whole designed contract and preparing for the jump to module 3.

Contents

  1. Documentation is part of the product
  2. Types of documentation and who each one serves
  3. Anatomy of an endpoint's reference
  4. Contract-driven documentation: OpenAPI
  5. A real fragment: GET /coffees in OpenAPI
  6. What a machine-readable specification unlocks
  7. Documentation as code
  8. Good writing practices
  9. Keeping documentation alive
  10. Taking stock of Aroma Store's contract

  1. Documentation is part of the product

An API has no visual interface. There are no buttons to explore and no menus hinting at what you can do. For a developer consuming it, the documentation literally is the product: if it is not documented, it does not exist.

Think about it from the other side. When SwiftShip's team sits down to integrate with Aroma Store, their experience consists of: reading, trying a curl, reading again, writing code, hitting an undocumented error, writing an email, waiting two days. Every one of those steps is cost. What good documentation reduces is not "annoyance": it is time to the first correct call, the metric that decides whether your API gets adopted or abandoned.

Practical consequences of taking it seriously:

  • It is planned and estimated like any other feature. An undocumented endpoint is not finished.
  • It is tested: the examples are run, not copied from memory.
  • It has an owner and it is reviewed in pull requests.
  • It reduces support: every question that arrives by email is a question the documentation failed to answer, and the right response is not to answer the email but to fix the documentation.

  1. Types of documentation and who each one serves

The most common mistake is writing a single giant document. There are different readers, at different moments, with incompatible needs.

Type Reader When they read it Question it answers
Quick start A new developer The first 15 minutes How do I make my first call?
Concept guide An integrator When starting the design How does this domain work?
Tutorials An integrator When implementing a use case How do I do a complete flow?
Reference Everyone Constantly, while programming Which parameters does this endpoint accept?
Changelog An already active integrator When upgrading, or when something fails What has changed?
Runnable examples Everyone When testing Can I see this working right now?

Quick start

The goal is one single thing: one correct call in under five minutes. No architecture, no theory.

## Your first call to the Aroma Store API

1. Get your key from the developer portal.
2. Run:

curl -H "Authorization: Bearer YOUR_TOKEN"
"https://api.aromastore.example/v1/coffees?limit=3"

3. You should receive:

{ "data": [ { "id": "cof_001", "name": "Ethiopia Yirgacheffe", "priceEuros": 14.50 } ], "total": 137 }

Working? Carry on with [Create your first order](./tutorial-order).

Concept guide

It explains the mental model, which no reference conveys. For Aroma Store: what a cart is and how it differs from an order, the order state machine, the review moderation cycle, how signed webhooks work, what it means for identifiers to be opaque. Without this, the integrator infers the model by trial and error, and infers it wrongly.

Tutorials

They walk through a complete use case from start to finish: "From cart to paid order", with the seven chained calls, their real responses and the likely errors at each step. It is what people are most grateful for and what gets written least.

Changelog

One entry per change, with the date, the type (added / changed / deprecated / removed / fixed) and a link to the migration guide where relevant:

## 2026-04-02
### Added
- `GET /v1/coffees` accepts the `available` filter (boolean).
- Coffees include `averageRating` and `reviewCount`.

### Deprecated
- The `price` field in the coffee representation. Use `priceEuros`.
  Planned withdrawal: 2027-04-02. See the [migration guide](./migration-price).

It is the cheapest document to maintain and the one that builds the most trust: it shows that the API is alive and that changes are announced.

  1. Anatomy of an endpoint's reference

The reference is what gets consulted daily. Every endpoint needs all of these elements; if one is missing, somebody will end up asking about it by email.

Element Detail
Method and path GET /v1/coffees/{coffeeId}
Description One sentence saying what it does and what it is for
Permissions Which token or role is needed
Path parameters Name, type, format, example
Query parameters Name, type, whether required, default, accepted values, maximums
Headers The ones it accepts or requires (Idempotency-Key, Accept-Language…)
Request body Complete schema, mandatory fields, validations
Success response Code, relevant headers and a complete example
Error responses Every possible code with its error code
Limits Rate limiting, maximum sizes, cost
Idempotency Whether it is idempotent, and how that is guaranteed
Examples A complete, copyable, working curl

An abbreviated example of how it looks for Aroma Store:

### POST /v1/orders/{orderId}/payment

Pays for a pending order. It creates the payment resource associated with the
order and, if it completes, changes the order's status to `paid` and emits the
`order.paid` event towards SwiftShip.

**Permissions:** the customer who owns the order, or an internal panel token.

**Idempotency:** mandatory. You must send `Idempotency-Key` with a UUID that is
unique per payment attempt. Retries with the same key and the same body return
the original response with `Idempotent-Replay: true`.

**Path parameters**

| Name | Type | Description |
|---|---|---|
| `orderId` | string | The order's identifier. E.g.: `ord_5001` |

**Body**

| Field | Type | Required | Description |
|---|---|---|---|
| `method` | string | Yes | `card` or `bank_transfer` |
| `cardToken` | string | If `method` is `card` | The gateway's token |

**Responses**

| Code | When | Error `code` |
|---|---|---|
| `201` | Payment completed (`Location` header) | — |
| `400` | Invalid data or missing `Idempotency-Key` | `invalid_data`, `idempotency_key_required` |
| `401` | No valid authentication | `not_authenticated` |
| `403` | The order is not yours | `insufficient_permissions` |
| `404` | The order does not exist | `order_not_found` |
| `409` | The order is already paid or a payment is in progress | `order_already_paid`, `operation_in_progress` |
| `422` | `Idempotency-Key` reused with a different body | `idempotency_key_reused` |
| `502` | The payment gateway does not respond correctly | `service_unavailable` |

**Example**

curl -i -X POST "https://api.aromastore.example/v1/orders/ord_5001/payment"
-H "Authorization: Bearer $AROMA_TOKEN"
-H "Idempotency-Key: 5f3b9c2a-1d7e-4a44-9f30-8b1c2d3e4f50"
-H "Content-Type: application/json"
-d '{ "method": "card", "cardToken": "tok_visa_4242" }'

The error column is the one most often left out and the one that prevents the most incidents: without it, the integrator discovers the 409 the day a user clicks twice.

  1. Contract-driven documentation: OpenAPI

Everything above can be written by hand in Markdown. It works, and for a small API it may be enough. But there is an alternative that changes the rules of the game: writing the contract in a format machines understand.

OpenAPI (formerly Swagger) is a specification —today at version 3.1— for describing an HTTP API in YAML or JSON: its paths, methods, parameters, data schemas, responses, errors and security. It is not documentation about the API: it is the API described formally, and readable documentation is only one of its outputs.

graph TD
    O["<b>openapi.yaml</b><br/>the contract"] --> D["Browsable reference<br/>documentation"]
    O --> C["Generated clients<br/>(JS, Java, Python…)"]
    O --> M["Simulated servers<br/>(mocks)"]
    O --> T["Automated contract<br/>tests"]
    O --> V["Request and response<br/>validation"]
    O --> G["Gateway and portal<br/>configuration"]

The difference from hand-written documentation is one of nature, not of degree: a Markdown document describes the contract and can lie; an OpenAPI specification is the contract and can be verified against the implementation automatically.

This lesson stops at the why and at an illustrative fragment. Swagger and OpenAPI in depth —editors, generators, the interactive interface, good writing practices— are lesson 05-02, and using the contract for mocks and automated tests is 05-04.

  1. A real fragment: GET /coffees in OpenAPI

This is what the contract of the coffee collection we designed in 02-06 looks like, written in OpenAPI 3.1:

openapi: 3.1.0
info:
  title: Aroma Store API
  version: 1.7.0
  description: |
    REST API of the speciality coffee shop Aroma Store.
    All amounts are in euros with two decimals and all dates
    are in ISO-8601 UTC.
servers:
  - url: https://api.aromastore.example/v1
    description: Production

paths:
  /coffees:
    get:
      summary: Lists the coffee catalogue
      description: |
        Returns the catalogue's coffees, filtered, sorted and paginated.
        Pagination is mandatory: if `limit` is not given, 20 is applied.
      operationId: getCoffees
      tags: [Coffees]
      parameters:
        - name: origin
          in: query
          description: Filters by country of origin. Accepts several comma-separated values.
          schema: { type: string }
          example: Colombia
        - name: roast
          in: query
          description: Filters by roast level. Accepts several comma-separated values.
          schema:
            type: string
            example: light,medium
        - name: priceMin
          in: query
          description: Minimum price in euros, inclusive.
          schema: { type: number, minimum: 0 }
        - name: priceMax
          in: query
          description: Maximum price in euros, inclusive.
          schema: { type: number, minimum: 0 }
        - name: q
          in: query
          description: Text search over name, origin and tasting notes.
          schema: { type: string, minLength: 2, maxLength: 100 }
        - name: sort
          in: query
          description: |
            Sorting field. Prefix with `-` for descending order.
            Ties in the ordering are always broken by `id` ascending.
          schema:
            type: string
            enum: [name, -name, priceEuros, -priceEuros, stock, -stock, createdAt, -createdAt]
            default: name
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
      responses:
        '200':
          description: List of coffees.
          headers:
            Link:
              description: Pagination links (RFC 8288) with rel next, prev, first and last.
              schema: { type: string }
          content:
            application/json:
              schema:
                type: object
                required: [data, total]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Coffee' }
                  total:
                    type: integer
                    description: Total number of elements matching the filter.
              example:
                data:
                  - id: cof_001
                    name: Ethiopia Yirgacheffe
                    origin: Ethiopia
                    roast: light
                    priceEuros: 14.50
                    stock: 120
                    tastingNotes: [citrus, floral, black tea]
                    createdAt: '2026-01-15T08:30:00Z'
                total: 137
        '400':
          description: Invalid query parameter.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              example:
                error:
                  code: invalid_parameter
                  message: "The 'limit' parameter cannot exceed 100."
                  details: []

components:
  parameters:
    limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
    offset:
      name: offset
      in: query
      schema: { type: integer, minimum: 0, maximum: 10000, default: 0 }
  schemas:
    Coffee:
      type: object
      required: [id, name, origin, roast, priceEuros, stock]
      properties:
        id:
          type: string
          pattern: '^cof_[a-zA-Z0-9]+$'
          description: Opaque identifier. Do not parse it.
        name: { type: string, maxLength: 120 }
        origin: { type: string }
        roast:
          type: string
          enum: [light, medium, dark]
          description: New values may be added without prior notice.
        priceEuros: { type: number, minimum: 0, multipleOf: 0.01 }
        stock: { type: integer, minimum: 0 }
        tastingNotes:
          type: array
          items: { type: string }
        createdAt: { type: string, format: date-time }
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, details]
          properties:
            code: { type: string, example: coffee_not_found }
            message: { type: string }
            details: { type: array, items: { type: object } }

Notice how much of this module's contract is encoded here, and verifiably so:

  • The default limit of 20 and maximum of 100, and the offset cap (02-06).
  • The id tie-break in the ordering, documented in the description.
  • The data/total envelope (02-05) and the error format with code/message/details (02-04).
  • The identifier's pattern and the opacity warning (02-02).
  • Amounts with multipleOf: 0.01, that is, two decimals (02-05).
  • The warning that the roast enumeration may grow (02-07).
  • The Link header as a declared part of the response.

The $refs to components avoid repeating limit, offset, Coffee and Error on every endpoint: they are written once and referenced, which is exactly the consistency 02-01 asked for, now guaranteed by construction.

  1. What a machine-readable specification unlocks

Output What it is Benefit Covered in
Browsable reference Generated HTML documentation Never falls out of sync with the contract 05-02
Interactive interface "Try it" from the browser A first call without writing code 05-02
Generated clients An SDK in several languages The consumer writes no HTTP code 05-02
Simulated servers A mock that responds according to the contract The SPA moves forward without waiting for the server 05-04
Contract tests Verify the implementation against the specification They detect drift automatically 05-04
Runtime validation Middleware validating requests and responses Consistent errors without writing them by hand 03-04
Style linters Check the style guide's conventions The style guide stops being voluntary 05-05
Gateway configuration Routes, limits and security imported Less duplicated configuration 05-06

The most important one for an API-first project like ours is the mock: with the openapi.yaml finished, the SPA and Aroma Mobile can start integrating on day one against a simulated server while the server team implements. That is what makes "design before you program" not lost time but parallelism gained.

  1. Documentation as code

Documentation is treated exactly like code:

  • It lives in the repository, next to the implementation. openapi.yaml at the root, guides in docs/.
  • It is versioned with Git, so the history answers "since when has it said this?".
  • It is reviewed in pull requests: an API change that touches neither the contract nor the changelog is not approved.
  • It is validated in continuous integration: the specification is checked syntactically, a style linter is run and the contract tests are executed.
  • It is published automatically when merged into the main branch.

Typical structure of the Aroma Store repository:

aroma-store-api/
├── openapi.yaml              # the contract
├── CHANGELOG.md              # changes by date
├── docs/
│   ├── style-guide.md        # the conventions from 02-01
│   ├── quick-start.md
│   ├── concepts/
│   │   ├── order-states.md
│   │   ├── idempotency.md
│   │   └── webhooks.md
│   ├── tutorials/
│   │   └── from-cart-to-paid-order.md
│   └── migrations/
│       └── migration-price.md
└── src/                      # the implementation (module 3)

The detail that makes this genuinely work: the quality gate in CI. If the pipeline fails when the implementation does not comply with the contract, documentation stops depending on goodwill. It is set up in 05-05.

  1. Good writing practices

Real, copyable examples. No <YOUR_ID_HERE> mixed in with fake data. Use the domain's identifiers (cof_001, ord_5001, cus_842) consistently across the whole documentation: whoever reads a tutorial recognises the same data in the reference.

# ✗ Useless: it cannot be run and it does not say what it returns
GET /coffees?params

# ✓ Copyable, runnable, with the expected response
curl -H "Authorization: Bearer $AROMA_TOKEN" \
     "https://api.aromastore.example/v1/coffees?roast=medium&limit=2"

A complete curl, always. With the authentication header, the URL in quotes (because of the &, as we saw in 01-03) and the whole body. It is the lowest common denominator: it works on any system and it assumes no programming language.

Document the errors as much as the successes. It is the most visible difference between professional and amateur documentation.

Explain the why, not just the what. "Idempotency-Key is mandatory because a retry after a timeout could charge twice" teaches; "Idempotency-Key: string, required" merely informs.

No "TBD", "to be documented" or "pending". A declared gap is worse than an absence: the reader wastes time trusting that it will turn up.

Consistency with the style guide. If the guide says camelCase, no example carries snake_case. Documentation is where inconsistencies show, and where they destroy trust fastest.

Write for someone who does not know your domain. The first use of "cart", "item" or "moderation" deserves a definition. SwiftShip's team knows about logistics, not about speciality coffee.

Careful with screenshots. They age badly and they cannot be searched or copied. Prefer code blocks.

  1. Keeping documentation alive

The enemy has a name: drift, the gap that opens between what the documentation says and what the API does. A badly documented API is a problem; an API that is badly documented but looks well documented is worse, because the integrator trusts it and fails.

Generated versus hand-written

Approach How it works For Against
Contract first You write openapi.yaml and documentation, mocks and validation come out of it Consistent with API-first; allows mocks before implementing Requires discipline so that the code follows the contract
Code first You annotate the controllers and the specification is generated Hard for it to fall out of sync with the implementation It documents what exists, not what was agreed; it arrives late
Mixed A hand-written contract + tests that verify the implementation The best of both You have to set up the contract tests

Aroma Store uses the mixed approach: a hand-written openapi.yaml —it is the source of truth, consistent with the API-first stance of 02-01— and contract tests in CI that fail if the implementation deviates. Guides and tutorials are always written by hand: no generator explains why a cart is not an order.

Detecting drift

Technique What it detects Where it is covered
Contract tests in CI Responses that do not comply with the schema 05-04
Response validation in staging New undocumented fields 03-04
Running the documentation's examples as tests Stale examples 05-04
An OpenAPI linter Broken conventions, missing descriptions 05-05
Mandatory review on every PR that touches the API Undocumented changes Process
Usage metrics compared with documented endpoints Undocumented "ghost" endpoints 04-07

Warning signs

  • The changelog has had no entries for months, but the API has changed.
  • The examples use fields that no longer exist.
  • There are endpoints in production that do not appear in the specification.
  • The real error responses do not match the documented ones.
  • The team answers questions over chat that the documentation should be answering.

  1. Taking stock of Aroma Store's contract

This is what we have designed over the course of module 2, and it is exactly what module 3 is going to implement.

Method and principles (02-01). An API-first approach, identified consumers (SPA, Aroma Mobile, internal panel, SwiftShip), resources extracted from the domain and a written style guide.

Resources and URIs (02-02). A complete map of 24 URIs on https://api.aromastore.example/v1, lowercase plurals, kebab-case, a maximum of two levels of nesting, singular singletons, opaque prefixed identifiers and non-CRUD actions modelled as sub-resources with POST (/payment, /cancellation, /approval, /rejection).

Methods (02-03). GET, POST, PUT, PATCH, DELETE, HEAD and OPTIONS assigned resource by resource; PATCH with JSON Merge Patch; soft deletes invisible from the outside; Idempotency-Key mandatory on POST /orders and POST /orders/{id}/payment.

Codes (02-04). The codes that actually get used, with 201 + Location, 409 for state conflicts, the 401/403 and 404/410 distinctions; an in-house error format {"error": {"code", "message", "details"}} against problem+json; a catalogue of 28 business error codes.

Representations (02-05). camelCase, ISO-8601 UTC, euros with two decimals, extensible snake_case enumerations, null versus absent, the data/total envelope, the criteria for embedding versus linking, _links for selective hypermedia, expand and fields, and negotiation with Accept, Accept-Language, Accept-Encoding and Vary.

Collections (02-06). Explicit filters, Min/Max and From/To ranges, multi-value with commas, sort with an id tie-break, offset for /coffees and cursors for /orders, total in the body and navigation in the Link header, ?q= for search, and default and maximum limits.

Evolution (02-07). The version in the path, two live versions at most, 12 months of deprecation, Deprecation and Sunset headers and shutdown with 410 Gone.

Documentation (02-08). openapi.yaml as the source of truth, hand-written guides and tutorials, a changelog by date, everything in the repository and validated in CI.

Common Mistakes and Tips

  • Leaving documentation until the end. That end never arrives. Document the endpoint in the same pull request that creates it.
  • Documenting only the happy path. Errors are half the integrator's work: undocumented 409s and 422s generate incidents nobody can explain.
  • Examples that cannot be run. Test every curl in the documentation. If one example fails, you have lost the reader's trust in everything else.
  • Confusing the reference with a guide. The reference says what an endpoint accepts; it never explains how to chain six calls to pay for an order. You need both.
  • Generating documentation from the code and calling it good. It documents what exists, bugs included, and it says nothing about concepts or intent.
  • Not documenting the limits. Rate limits, maximum sizes, the maximum limit and expansion depth are part of the contract: without them, the integrator discovers them with a 400 in production.
  • Forgetting the changelog. It is the cheapest document and the one an external consumer appreciates most.
  • Tip: measure the "time to the first correct call". Sit down with somebody who does not know the API, give them the documentation and time them in silence. You will learn more in twenty minutes than in three meetings.
  • Tip: treat every support question as a documentation failure. The answer is not to reply to the email, it is to fix the document and then reply with the link.

Exercises

Exercise 1: write an endpoint's reference

Write the complete reference documentation for POST /v1/coffees/{coffeeId}/reviews with all the elements from section 3. Use the contract designed in this module: the body carries rating (an integer from 1 to 5) and comment (text of up to 5,000 characters); the review is created with the status pending_moderation; only customers who have bought that coffee can review it.

Exercise 2: complete the OpenAPI contract

Extend the fragment from section 5 by adding the GET /coffees/{coffeeId} operation: the path parameter, a 200 response with the reused Coffee schema, a 404 response with the Error schema and an example of the coffee_not_found error, and a 304 response for conditional caching. Reuse the existing components.

Exercise 3: detect drift

A developer joins Aroma Store and finds this. Identify all the documentation problems and propose the concrete remedy and the process that would stop it happening again.

  • The reference says GET /v1/coffees returns an array; the API returns {"data": [...], "total": n}.
  • There is no mention at all of the expand parameter, which nevertheless works.
  • The POST /v1/orders example does not include the Idempotency-Key header, which is mandatory.
  • The error table for POST /v1/orders/{id}/payment only lists 400 and 500.
  • The changelog stops eight months ago.
  • There is a /v1/promotions endpoint in production that appears nowhere.

Solutions

Solution 1

### POST /v1/coffees/{coffeeId}/reviews

Creates a review of a coffee. The review is created with the status
`pending_moderation` and does not appear in public listings until a moderator
approves it with `POST /v1/reviews/{reviewId}/approval`.

**Permissions:** an authenticated customer who has bought the coffee in an order
with the status `shipped`. Otherwise `403` is returned.

**Idempotency:** not mandatory. `Idempotency-Key` is optional and recommended to
avoid duplicate reviews from a double form submission.

**Path parameters**

| Name | Type | Description |
|---|---|---|
| `coffeeId` | string | The coffee's identifier. E.g.: `cof_001` |

**Body**

| Field | Type | Required | Validation |
|---|---|---|---|
| `rating` | integer | Yes | Between 1 and 5, both included |
| `comment` | string | Yes | Between 10 and 5,000 characters |

**Responses**

| Code | When | Error `code` |
|---|---|---|
| `201` | Review created (`Location` header) | — |
| `400` | Validation failed | `invalid_data` |
| `401` | No authentication | `not_authenticated` |
| `403` | The customer has not bought this coffee | `insufficient_permissions` |
| `404` | The coffee does not exist | `coffee_not_found` |
| `409` | The customer has already reviewed this coffee | `duplicate_review` |
| `410` | The coffee has been discontinued | `coffee_discontinued` |
| `429` | Request limit exceeded | `rate_limit_exceeded` |

**Limits:** a maximum of 5 reviews per customer per day.

**Example**

curl -i -X POST "https://api.aromastore.example/v1/coffees/cof_001/reviews"
-H "Authorization: Bearer $AROMA_TOKEN"
-H "Content-Type: application/json"
-d '{ "rating": 5, "comment": "Citrus and floral, spectacular in a V60." }'

**Response**

HTTP/1.1 201 Created Location: https://api.aromastore.example/v1/reviews/rev_101

{ "id": "rev_101", "coffeeId": "cof_001", "customerId": "cus_842", "rating": 5, "comment": "Citrus and floral, spectacular in a V60.", "status": "pending_moderation", "createdAt": "2026-03-14T10:30:00Z", "_links": { "self": { "href": "/v1/reviews/rev_101" }, "coffee": { "href": "/v1/coffees/cof_001" } } }

Notice that a new error code has appeared, duplicate_review (409): documenting forces you to close decisions the design had left open. That is one of the great benefits of writing the reference before implementing.

Solution 2

  /coffees/{coffeeId}:
    get:
      summary: Retrieves one specific coffee
      operationId: getCoffeeById
      tags: [Coffees]
      parameters:
        - name: coffeeId
          in: path
          required: true
          description: The coffee's opaque identifier.
          schema:
            type: string
            pattern: '^cof_[a-zA-Z0-9]+$'
          example: cof_001
        - name: If-None-Match
          in: header
          required: false
          description: The ETag of a previous copy, for conditional caching.
          schema: { type: string }
          example: '"a1b2c3d4"'
      responses:
        '200':
          description: The requested coffee.
          headers:
            ETag:
              description: Version identifier of the representation.
              schema: { type: string }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Coffee' }
              example:
                id: cof_001
                name: Ethiopia Yirgacheffe
                origin: Ethiopia
                roast: light
                priceEuros: 14.50
                stock: 120
                tastingNotes: [citrus, floral, black tea]
                createdAt: '2026-01-15T08:30:00Z'
        '304':
          description: |
            The representation has not changed since the version given in
            `If-None-Match`. No body.
          headers:
            ETag:
              schema: { type: string }
        '404':
          description: No coffee exists with that identifier.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              example:
                error:
                  code: coffee_not_found
                  message: "No coffee exists with the identifier 'cof_999'."
                  details: []
        '410':
          description: The coffee existed and has been permanently discontinued.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              example:
                error:
                  code: coffee_discontinued
                  message: "The coffee 'cof_001' was withdrawn from the catalogue on 2026-02-01."
                  details: []

Note that Coffee and Error are reused with $ref: when a field is added to the Coffee schema tomorrow, it automatically appears in both operations and in the generated documentation. That is consistency guaranteed by construction, not by human review.

Solution 3

Problem Severity Remedy Prevention
The reference says array and the API returns an envelope Critical: every new integrator writes code that fails on the first call Fix the schema in openapi.yaml and publish Contract tests in CI (05-04): a response that does not comply with the schema must break the build
expand undocumented High: invisible functionality that nobody is guaranteeing to maintain either Document it with its rules (one level, maximum 3, 400 if unknown) Mandatory PR review: no new parameter is merged without contract
Missing Idempotency-Key in the example High: the copied example returns 400 Fix the example and mark the header as mandatory Run the examples as tests in CI
Incomplete error table High: the 409 order_already_paid shows up in production with no warning Complete it with 401, 403, 404, 409, 422 and 502 An endpoint template with the error table as a mandatory field
Abandoned changelog Medium: trust is lost and deprecations go unnoticed Rebuild it from the Git history and pick it up again A CI check: if openapi.yaml changes, CHANGELOG.md must change
Ghost endpoint /v1/promotions Critical: an undocumented, unversioned and probably security-unaudited surface Decide: document it or withdraw it. There is no third option Compare the real routes (metrics from 04-07) with those in the specification and alert on the differences

The process that prevents all of it, in one sentence: the specification is the source of truth, it lives in the repository, it is validated on every pull request and the pipeline fails if the implementation does not comply with it. Without an automatic gate, drift is only a matter of time.

Conclusion

Documentation is not what you write after programming: it is the visible face of a product that has no interface. You now know that different readers need different documents —quick start, concepts, tutorials, reference, changelog—, what each endpoint's reference must contain (including all of its errors, which is what gets left out most), and why writing the contract in OpenAPI changes the nature of the matter: it stops being a text that describes the API and becomes the contract itself, out of which come the browsable documentation, the clients, the mocks, the tests and the gateway configuration. With documentation as code, reviewed in pull requests and validated in CI, drift stops depending on goodwill.

With this we close module 2. You have designed, piece by piece and on paper, the complete contract of the Aroma Store API: its working method and its style guide, its 24 URIs with the actions modelled as sub-resources, its methods with the idempotency of payment resolved, its table of codes and its catalogue of business errors, its representations with selective hypermedia, its filtered and paginated collections, its versioning and deprecation policy, and its documentation. None of this has needed a single line of server code yet, and that was exactly the idea: in API-first, the contract goes first. Now it has to be honoured. In module 3, Developing RESTful APIs, we will set up the environment with Node.js 20, bring up the server with Express and turn every decision from this module into code: the routes of the URI map, the validation that returns invalid_data with its details, the persistence layer that converts cents into euros, the authentication that distinguishes 401 from 403, the error middleware that emits the format we have fixed and the tests that verify the implementation respects the contract. We start at the beginning: 03-01, Setting Up the Development Environment.

REST API Course: Principles of Designing and Developing RESTful APIs

Module 1: Introduction to RESTful APIs

Module 2: Designing RESTful APIs

Module 3: Building RESTful APIs

Module 4: Best Practices and Security

Module 5: Tools and Frameworks

Module 6: Case Studies and Projects

© Copyright 2026. All rights reserved