Designing APIs clients don't have to work around
A short set of rules we hold ourselves to when an API is going to outlive the first client who asked for it.
Most integration pain doesn't come from a missing endpoint. It comes from an API shaped around the first caller instead of the domain. We write ours against the domain first, then let the first client prove it out.
The failure mode is easy to recognise once you've seen it. An endpoint called
/dashboard-summary returns exactly the six numbers one screen needed in 2024.
Two years later there are four callers, three of them ignore half the payload,
and nobody can change the shape because a mobile app in the field depends on the
field ordering. The API stopped describing the business and started describing a
screen that no longer exists.
Model the domain, not the screen
The question we start from is what things exist in this business and what can happen to them. A savings group has members, contributions, loans and repayments. Those nouns will still be true after the interface is redesigned twice, so they are what the API should be made of.
Screens are then assembled from those resources. That occasionally means a client makes two calls where one would have done, which is a real cost and worth paying, because the alternative is an endpoint per screen and a rewrite every redesign. Where a chatty client genuinely hurts, the fix is a deliberate composite endpoint added later, named for what it returns, not a domain model bent around the current layout.
Rules we hold ourselves to
- Names come from the business, not the database. If the team says "merry-go-round"
and the table is called
rotational_groups, the API says merry-go-round. An API is a description of the domain, and a leaked schema name is a description of a migration you'll regret. - Ids are opaque and stable. Callers should never parse them, and they should never be reused once issued.
- Errors are specific and machine-readable. A stable error code, a human-readable message, and the field at fault. "400 Bad Request" with an empty body sends an integrator to read your source code, and they can't.
- Everything that can grow is paginated from day one. Collections that are small in testing are not small in production, and adding pagination later is a breaking change dressed up as an improvement.
- Anything that moves money takes an idempotency key. On the connections our users have, a request that times out has not necessarily failed. The client must be able to retry safely without a second charge, and that guarantee has to live on the server, not in the client's fingers crossed.
- Timestamps are UTC and ISO 8601. Local time in a payload is a bug waiting for a timezone conversation.
- Changes are additive by default. New optional fields are fine forever. Removing a field or changing its meaning is a new version, and old versions get a deprecation window measured in months.
Write the caller before the server
The cheapest way to find out an API is awkward is to use it before it exists. Before endpoints are built, we write the call sequence a real client would make to accomplish a real task: register a member, record a contribution, show a balance. It takes an afternoon and it consistently exposes the same class of problem, which is a resource that can't be reached without information the caller doesn't have yet.
Those chicken-and-egg gaps are almost free to fix on a whiteboard and expensive to fix once three services depend on the sequence.
Documentation is part of the API
An endpoint nobody can call without asking us a question is not finished. That
means a real request and a real response for every operation, with realistic
values rather than "string" and "foo", error responses documented alongside
the success case, and authentication explained in enough detail that an
integrator gets a token without a meeting.
The test we use is simple: can a competent developer who has never spoken to us integrate the thing from the documentation alone? If the answer is no, the API is finished only for the person who wrote it.
The point of all this
APIs outlive the projects that produce them. The client who commissioned it will add a second application, get acquired, or hand it to a partner. Every one of those futures is easier when the interface describes the business rather than the first screen somebody drew.
None of these rules are novel. They're just the ones we've watched get skipped, and then watched get paid for later.