API documentation usually gets written at the end, in a rush and from memory. Three months later it no longer matches the real behavior, and whoever integrates the service ends up reading the code or asking in chat.

The contract first

OpenAPI is a YAML or JSON specification that describes endpoints, parameters, request and response bodies, error codes and authentication. Write that file before any code. The backend and frontend teams review it together and argue over names and error shapes while they are still easy to change. Only then does implementation begin.

This is how an endpoint looks in the file, error response included:

paths:
  /orders/{orderId}:
    get:
      summary: Fetch an order
      parameters:
        - name: orderId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: The order exists
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
        "404":
          description: No order has that identifier
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
              example:
                type: https://api.example.com/errors/order-not-found
                title: Order not found
                status: 404
                detail: No order exists with the given identifier.
                instance: /orders/3f2b8c1e-5a7d-4c3e-9b1a-2d6e8f0a4b7c
                traceId: 7c1f0a52-8d3e-4b6a-9e21-5f0c7a3d1b84

Once the contract is agreed, the frontend team stops waiting for the service: it generates a mock server from the file, with a tool such as Prism, and moves ahead with realistic responses from day one.

What the same file gives you

The specification feeds several things at once. Tools like Swagger UI or Redoc publish browsable documentation where calls can be tried out. Code generators produce typed clients in TypeScript, Java or whatever language the consumer uses. Contract tests compare real responses against what the file describes and raise a flag when someone changes a field without updating it.

One OpenAPI file feeds browsable documentation, a mock server, typed clients, contract tests and pipeline validation.

On Spring Boot projects it is common to generate the specification from annotated code, which works if the generated file gets reviewed on every change as part of code review.

Common mistakes

A frequent one is documenting only the happy path: the 400, 401, 404 and 409 responses with their format are what an integrator needs most. Another is skipping versioning, because a change that breaks clients needs a new API version and a changelog entry. A third is publishing placeholder examples, such as string or foo, instead of values that look like real ones.

It also helps to define one error format for the whole service. RFC 9457 (Problem Details) is a good starting point. A stable type identifies the kind of error, title and detail carry the message and, as an extension, you can add a request id such as traceId to follow the case in the logs. The 404 above already uses the standard fields and that extension.

Review in the pipeline

A validation step in the continuous integration pipeline, using a linter such as Spectral, keeps out files that are malformed or that break the team's rules. Setting it up takes an afternoon. The minimal configuration starts from the rules the tool already ships:

# .spectral.yaml
extends: ["spectral:oas"]

The pipeline step is a single line, npx @stoplight/spectral-cli lint openapi.yaml, and it fails when the file breaks a rule.

Do you have a service without a clear contract? Tell us which API you integrate today and we will write the contract with you. In our backend and API development service we design and build REST APIs following OpenAPI specifications.

Back to the blog