ISSUE 2026-09-27SERIES BREC 21TYPE BRIEFgarnetgrid.com
The question underneath the label
Insights
Everyone has APIs. "API-first" claims something narrower and more expensive: that the contract is designed and reviewed before the implementation exists, and outranks the code when the two disagree. This is what that discipline actually buys, the places it quietly fails in year two, and the cases where reaching for it is the wrong instinct.
Almost every system has an API. The label "API-first" claims something narrower and more expensive: that the interface is designed, reviewed and agreed before the implementation exists, and that when the code and the contract disagree, the contract wins. That is a real commitment with real costs, and the interesting question is not whether it sounds sensible. It is whether the discipline survives contact with the second year, when a consumer you did not plan for is in production and someone needs a field removed.
What committing to the contract actually means
In practice, API-first means the specification is a reviewable artefact that lands before the handler does. An OpenAPI document, a set of protobuf definitions, a GraphQL schema: the format matters far less than the sequencing.
Three things follow, and they are the whole benefit. A consumer team can start the day the spec merges, against a mock generated from it, instead of waiting for a server. Design arguments happen in a pull request on a schema file, which is cheap, rather than after the database migration has shipped, which is not. And the spec becomes an obligation, something you owe people, with a name and a version, rather than a description of whatever the code happens to do this week.
The sequencing does something specific if the system will run on hardware you do not operate. An integrator on the far side of a customer's firewall cannot ask your engineers what a field means, and if the deployment is genuinely offline it cannot reach a hosted developer portal either. The contract and a runnable mock have to travel inside the build artefact. That constraint is unusual, and it puts the contract closer to being the product than the code is.
Drift is the default state
There are two ways to end up with a specification, and neither is free.
Hand-write it and implement to it, and you now maintain two descriptions of one thing. They diverge silently, because nothing executes the document. The first symptom is usually a consumer following the spec correctly and getting a 400.
Generate it from the code instead, and the spec cannot lie, but it also cannot be reviewed before there is code to generate it from, which gives up the main reason you wanted this. Generated specs leak the implementation too: your ORM's nullable columns, your internal enum names, whatever your serialiser does with an empty collection.
The arrangement that holds is to make the spec executable rather than descriptive. Generate server stubs and client types from it. Validate requests and, more importantly, responses against it in tests and in staging, so a mismatch fails a build instead of arriving as a support ticket. Add consumer-driven contract tests, so a consumer's expectations break the producer's pipeline while somebody can still do something about it.
One failure mode is worth naming because it is so common: response validation that only exercises the happy path. Error responses are the least-tested part of most APIs, the part that drifts fastest, and the part consumers handle worst.
Versioning is where these programmes die
Teams classify changes as additive or breaking with more confidence than the classification deserves. A surprising number of routinely-shipped "minor" changes will break somebody.
Underneath that list is a harder problem: you cannot classify a change without knowing what your consumers actually read. With no field-level usage telemetry per client, removing a field is a guess dressed as a decision. Instrumenting that is cheap, and I would put it ahead of choosing a versioning scheme.
The posture that causes least pain is to add rather than change, to keep old shapes working while anyone is still calling them, and to treat deprecation as a measured migration with dates and a named owner rather than an announcement. In protobuf, field numbers are permanent: retire one and never reuse the number.
On URL versions against header negotiation, my opinion and clearly an opinion: put the version in the path. It is inelegant, it is trivially visible in a log, and it does not get quietly mangled by an intermediary that does not understand your media types.
API-first is not microservices
These two get conflated constantly and the conflation is expensive. API-first is a discipline about designing boundaries. Microservices is a decision about deployment. You can have the first without the second, and that is often the right place to stop.
A well-specified boundary inside a single deployable gives you most of the design benefit. The call is a function call. A contract violation is a compile error. A refactor that spans the boundary is one commit and one review.
Turning that call into a network hop converts compile-time errors into 3am errors, and buys a list of new obligations: timeouts, retries, duplicate delivery, partial failure, serialisation cost, and a second place where authorisation has to be enforced. If you do split, every boundary owes you a timeout budget smaller than its caller's, retries with jitter on idempotent operations only, idempotency keys on writes, and an explicit decision about what the caller sees when the dependency is down.
How many deployables you should have depends on your team boundaries and release cadence, not on architecture fashion, and there is no honest general answer to it.
The parts nobody specifies and everybody needs
Most specifications describe resources well and operational semantics badly. The gaps are predictable.
One of these is a security question rather than an ergonomic one. The commonest serious bug in an otherwise careful API is an endpoint that validates the shape of an identifier and not the caller's right to it. Object-level authorisation belongs at the boundary, and tenant scoping belongs in the data access layer where it cannot be forgotten, rather than in each handler where it can.
When it is the wrong instinct
Three cases where reaching for a general-purpose API is the wrong instinct.
Bulk and analytical data movement. A request-response interface over millions of rows means reimplementing paging, resumption and consistency, badly, at the wrong layer. Ship files, a table the consumer can query, or a change stream.
A single consumer that is your own front end. A strictly general resource API then costs a fan-out of round trips per screen, and somebody eventually builds a composition layer to undo it. An endpoint shaped to a screen is not a sin. Keep the contract discipline and drop the pretence of generality.
A domain still moving weekly. Freezing a contract you do not yet understand buys a migration you did not need. Keep the boundary internal, where changing it is a refactor, until the shape settles.
Then there is the version that collects all the ceremony and none of the benefit: an internal API with a specification, a review process, a generated client, and no inventory of who calls it. The paperwork was never the point. Knowing who depends on you is.
If you do three things, do these. Pick one source of truth for the contract and make a build fail when the implementation disagrees with it. Record, per endpoint, who consumes it and which fields they read, before the day you want to remove one. Decide your breaking-change policy while it is still theoretical, because deciding it under pressure produces a version 2 that nobody migrates to. Everything else is proportionate. Specify errors, pagination and idempotency now, because retrofitting them is itself a breaking change. Leave the boundary as a function call until a team boundary or a release cadence actually forces a network hop. And accept that how much of this you need depends on your consumers and your volumes, which is a real answer even if it is not a satisfying one.