WebTech1223411 logo WebTech1223411Web tech, read closely
Back-End

How to Design a REST API Other Developers Enjoy Using

The best compliment an API can get is silence. Nobody files a ticket, nobody asks in the shared channel what a field means, and the integration that was estimated at two weeks ships in four days. That kind of API is rarely clever.

Abstract illustration for How to Design a REST API Other Developers Enjoy Using

The best compliment an API can get is silence. Nobody files a ticket, nobody asks in the shared channel what a field means, and the integration that was estimated at two weeks ships in four days. That kind of API is rarely clever. It is predictable, and predictability is a design choice you make in dozens of small decisions long before anyone writes client code.

I have spent most of my working life on the server side of these conversations, maintaining APIs that other teams and outside partners depend on. What follows is the short list of habits that, in my experience, separate an API people tolerate from one they actually enjoy using.

Name things the way your users already think about them

Resource names are the first thing a developer sees and the thing they will type hundreds of times. Use plural nouns for collections, keep them lowercase, and pick words from the domain rather than from your database. If your users talk about "orders", the endpoint is /orders, even if the table is called txn_header.

Nest only when the relationship is real and stable. /orders/481/items is clear. /customers/22/orders/481/items/3/discounts is a sign that the URL is carrying information that belongs in the response. Two levels of nesting cover almost every case.

Be boringly consistent with field names. If one endpoint returns created_at and another returns createdDate, every client now needs a mapping layer. Pick one case style, one timestamp format (ISO 8601 in UTC is the safe default) and one way of naming identifiers, then enforce it in review.

Use HTTP the way HTTP intends

Methods and status codes are a shared vocabulary. When you use them correctly, clients get caching, retries and sensible error handling for free from their HTTP libraries.

  • GET reads and never changes anything. Proxies and browsers may cache or repeat it.
  • POST creates or triggers an action. Return 201 Created with a Location header when a resource is made.
  • PUT replaces, PATCH partially updates, DELETE removes. PUT and DELETE should be idempotent, so repeating them is safe.
  • 4xx means the client must change something. 5xx means the server failed and a retry may help.

The most common mistake I see is returning 200 OK with an error message inside the body. Monitoring tools count it as a success, retry logic never fires, and the client team ends up parsing strings to work out what went wrong.

Errors are part of the interface

Developers spend more time reading your errors than your success responses, because errors are what they hit while building. A good error body has a stable machine-readable code, a human message, and enough detail to fix the problem. The Problem Details format in RFC 9457 is a sensible structure to adopt instead of inventing your own:

{"type": "/errors/invalid-field", "title": "Invalid field", "status": 422, "detail": "quantity must be between 1 and 99", "field": "quantity"}

Compare that with {"error": "Bad request"}. The first one tells the developer exactly which field to fix. The second one sends them to your support channel. When validation fails on several fields, return all of them at once rather than one per request. Making someone submit a form five times to discover five problems is the API version of a rude shop assistant.

Pagination, filtering and the cost of large lists

Any collection that can grow will eventually be too big to return at once. Decide on pagination before launch, because adding it later breaks every client that assumed the full list.

Offset pagination with ?page=3&limit=50 is easy to understand but drifts when items are inserted while someone is paging, and it gets slow on large tables because the database still scans the skipped rows. Cursor pagination, where the response includes an opaque next_cursor value, avoids both problems and is what I default to for anything that changes often. Whatever you choose, always cap the page size on the server. A client asking for limit=100000 should get your maximum, not a timeout.

Filtering and sorting should follow one pattern across all collections, such as ?status=shipped&sort=-created_at. When every list endpoint behaves the same way, developers learn it once.

The versioning advice I no longer follow

A lot of guides tell you to put /v1/ in every URL on day one and bump to /v2/ whenever something changes. I used to do exactly that, and I now think it causes more harm than it prevents.

Version bumps are expensive for everyone. Clients must migrate, you must run two codebases, and in practice the old version lives for years. The better habit is to design for additive change: new fields are optional, old fields are never repurposed, and clients are told to ignore fields they do not recognise. Done that way, most changes never need a new version at all.

Keep a version prefix if you like, but treat a new major version as a rare, planned event, not a routine release step. When you do deprecate something, announce it with a date, send a Deprecation header on affected responses, and watch your logs to see who is still calling the old behaviour before you remove it.

Documentation that matches reality

The fastest way to lose trust is documentation that disagrees with the live API. Generate reference docs from an OpenAPI description that is checked in CI against the real implementation, so the two cannot drift apart silently.

Reference docs are not enough on their own. Add a short getting-started guide that goes from API key to first successful call in under five minutes, with copy-paste examples in curl and one or two popular languages. Include realistic example responses, not "string" placeholders. A developer should be able to read one example and guess the shape of the rest.

Finally, give people a sandbox with test data. Being able to experiment without fear of charging a real card or emailing a real customer changes how quickly someone learns an API.

Performance and limits, stated up front

Rate limits are fine. Surprise rate limits are not. Publish them, return 429 Too Many Requests with a Retry-After header, and include remaining-quota headers on normal responses so clients can slow down before they hit the wall. If you want to see how a platform handles this kind of pressure at scale, our piece on how online game platforms scale their servers walks through the same ideas under much heavier traffic.

Support conditional requests with ETag and If-None-Match for resources that are read often and change rarely. A 304 Not Modified saves bandwidth on both sides and costs you almost nothing to implement.

None of this is glamorous. Consistent naming, honest status codes, helpful errors and stable contracts are the unexciting habits that make an API feel trustworthy. The developers who use it will not thank you directly. They will just build things on top of it, quickly, and that is the real measure of a good design. More on server-side work lives in our Back-End section.

KO
Kofi Oosterhuis

Kofi spent years keeping APIs and game servers alive through traffic spikes and the occasional bad deploy. He covers back-end design, scaling and networking, with a preference for boring systems that do not page anyone at night.

More posts by Kofi

More from the blog