Architecture

Designing API-first with an OpenAPI contract

An OpenAPI contract in the repository, linting and a breaking-change check in CI let a distributed team build both sides of an API at the same time.

BorisBy BorisDeveloper · 4 min readLees in het Nederlands
A cable-stayed bridge in black and white
Photo: Artem Militonian on Unsplash

API-first means you write and agree on the contract of an API before anyone builds it. The contract is an OpenAPI file in the repository, reviewed in a pull request and checked in CI. A team spread over several locations gets two things out of that: frontend and backend start at the same time, and the discussion is written down instead of happening in a call not everyone joined.

From contract to implementation

  1. Someone describes the new endpoint in the OpenAPI file: the path, parameters, request and response bodies, error codes and an example for each response.
  2. Frontend and backend review that change like any other pull request.
  3. After the merge, the backend builds the implementation and the frontend builds against a mock.
  4. CI checks that the implementation matches the contract.

Keep the contract in the same repository as the backend, so a change to code and contract fits in one pull request. If several teams or external parties consume the API, a separate repository with numbered releases usually works better.

The latest version of the specification is OpenAPI 3.2, but not every tool reads it yet. openapi-typescript, for example, only mentions 3.0 and 3.1. Stay on 3.1 until your whole toolchain handles 3.2.

For the developer who builds it, a ticket that says "build GET /orders/{id} according to the contract" is a lot clearer than "make an API for orders". How to write tickets like that is covered in writing tickets a remote developer understands.

Why it helps when you do not sit together

In a distributed team, waiting is expensive. If the frontend has to wait until the backend is done, you lose days. A question you cannot ask across the desk also tends to sit for half a day.

With a contract the frontend can start right away. Prism runs a mock server from your OpenAPI file and returns the examples you put in it. openapi-typescript generates the TypeScript types from the same file. When a field changes in the contract, the frontend gets a compile error on the next build instead of a bug in production.

The discussion about names, required fields and error codes lives in the pull request review. Whoever joins later can read there why something was chosen. That fits with working async, where decisions are written down as well.

A contract you can work with

This is what I look for when I review a contract:

  • every response has an example, including error responses,
  • each field states whether it is required and whether it can be null,
  • all endpoints use the same error format,
  • dates are ISO 8601 strings with a time zone,
  • pagination works the same way everywhere,
  • IDs are strings, even if they are numeric today.

You do not have to check this by hand. Spectral lints OpenAPI files with built-in rules, and you can add rules for your own conventions. Run it in CI and fail the build when the contract breaks a rule.

Versions without surprises

A new endpoint or a new optional field in a response is almost always safe. Breaking is anything that makes an existing client fail: removing or renaming a field, changing a type or making an optional request field required.

oasdiff compares two versions of your OpenAPI file and reports which changes break existing clients. There is a GitHub Action for it, so you see the problem in the pull request and not when a customer calls.

If you do have to break something, create a new version and keep the old one running for an agreed period. Mark the old operations with deprecated: true in the contract. Also send the Deprecation header, standardised in RFC 9745 in March 2025, and a Sunset header with the date the old version stops. Then even a team that never reads your documentation sees in the responses that something is changing.

Agree on the length of that period up front with whoever uses the API. For your own frontend that you release in the same week, a few days is enough. For mobile apps in the app stores or external customers, think in months.

Keeping contract and code in sync

The biggest risk is a contract that slowly drifts away from what the server does. Two agreements help: a pull request that changes the behaviour of the API updates the contract in the same pull request, and the reviewer looks at both, as described in code review in a remote team.

In your test environment you can also route traffic through Prism's validation proxy. It reports where the implementation differs from the OpenAPI file.

Starting with an existing API

  1. Put your existing API in an OpenAPI file. Generate it from your code if you can, then clean it up by hand.
  2. Add Spectral and oasdiff to your CI.
  3. Write the next new endpoint in the contract first, in its own pull request.
  4. Generate the frontend types from that file and delete the hand-written ones.

Sources

Frequently asked questions

What does API-first development mean?

You write and review the API contract first, usually as an OpenAPI file, and build the implementation afterwards. Frontend and backend can then work in parallel, with the frontend running against a mock server.

Which API changes are breaking?

Anything that breaks an existing client: removing or renaming a field, changing a type or making an optional request field required. A new endpoint or a new optional response field is usually safe.

How do you announce that an API version is going away?

Mark the operations as deprecated in the OpenAPI contract, send the Deprecation header from RFC 9745 and put the shutdown date in a Sunset header. Then keep the old version running for an agreed period.

About the author

Boris

Boris

Developer

Boris is a developer at Brainpower Group and works from Serbia inside client teams.

More from Boris
Contact
Bas Leijder HavenstroomMaarten ter VeldeBarry Willems

Tell us who you are looking for.

An introduction takes half an hour, online or at our office in Amsterdam. In the chat we usually reply within a few minutes.

Or call +31 20 210 1978hello@brainpowergroup.eu

Bas, Maarten and BarryLive now · replies within a few minutes

The chat opens once you send.