Designing a REST API before you write it
2 September 2026 · 5 min read · #backend #api #http
written from this video
A REST API is designed before it is programmed. That sounds obvious and almost nobody does it. The usual sequence is: open the editor, write a handler, decide the route as you type it, decide the status code when the handler returns, and discover three weeks later that /book/:id is singular while /projects is plural.
The alternative costs an afternoon. Open Insomnia or Postman, create the requests, run them against nothing, and argue with yourself about paths and payloads while it is still free to change your mind. No language, no framework, no business logic. Just the interface.
Resources are the nouns
Start from the wireframes, not the database. Read the Figma file and write down every noun. A project management tool gives you organizations, projects, tasks, users, tags. Those are your resources, and they are the only thing that appears in a path segment.
Everything after that follows a small number of rules that were settled twenty-five years ago and are still argued about weekly:
- Plural, always.
/organizations/42, not/organization/42. Even when you are fetching exactly one. The path names the collection; the ID selects from it. - Lowercase, hyphens, no underscores, no spaces.
Harry Potterbecomesharry-potter. URLs cross operating systems, proxies and shells, and case mismatch is a bug you cannot debug from the server side. - A slash means hierarchy.
/organizations/42/projectssays projects live under this organization. If your path doesn't read as a containment chain, it's wrong. - Version in the path.
https://api.example.com/v1/organizations. Not a rule, just what everyone does, which is the same thing.
Idempotency picks the method
The question "should this be PUT or PATCH" has a mechanical answer, and it comes from one property: does calling this a thousand times leave the server in the same state as calling it once?
GET is idempotent — it changes nothing. PUT and PATCH are idempotent — setting name = "B" a thousand times still leaves name = "B". DELETE is idempotent, and this is the one people get wrong: the second call returns 404, but a 404 is not a side effect. Nothing changed. The error is information about state, not a mutation of it.
POST is the only one that isn't. Every call creates a new row with a new ID. A thousand calls, a thousand books.
PATCH updates some fields; PUT replaces the whole representation, which means the client sends every field including the ones it doesn't care about. In practice PATCH wins almost everywhere, because single-page apps send partial JSON. Use PUT only when you actually mean full replacement.
Most teams use PUT and PATCH interchangeably and survive. It stops being survivable the moment someone outside your org integrates against your API and assumes you meant what the spec says.
Custom actions are POST, and they are not updates
Archiving an organization looks like PATCH /organizations/42 with {"status": "archived"}. It isn't. Archiving might cascade to projects, delete tasks, email every member. The client cannot simulate that by writing a field, and it shouldn't have to know it happens.
That's a custom action, and POST is the open-ended method the spec reserves for exactly this:
POST /v1/organizations/42/archive
POST /v1/projects/17/clone
Collection, instance, verb. The hierarchy still reads correctly.
And do not assume the status code from the method. archive returns 200 — it created nothing. clone returns 201 — it did. The status describes what happened, not which verb you used.
A list endpoint has three jobs
Every list endpoint needs pagination, sorting and filtering, and all three arrive as query parameters.
GET /v1/organizations?page=2&limit=20&sortBy=name&sortOrder=asc&status=active
The response carries the slice plus the metadata the client needs to render a pager:
{
"data": [],
"total": 5,
"page": 1,
"totalPages": 3
}
total is the count in the database, independent of the slice — that's what lets the UI say "showing 10 of 50". totalPages is what lets an infinite scroll know when to stop calling you.
Pagination is not decoration. Serializing a thousand rows to JSON is real CPU, real bytes, and a user who sees the first twenty anyway.
Sane defaults are the whole game
Every query parameter is optional, and the server fills in the blanks:
page -> 1
limit -> 20
sortBy -> createdAt
sortOrder -> desc
The sort default matters more than it looks. Without an explicit ORDER BY, the database returns rows in whatever order it feels like, so the same request twice returns different results and pagination silently drops records. createdAt desc is the correct default because it is what the user expects: newest first.
The same applies to bodies. POST /organizations should need name and nothing else. status defaults to active, because what else would a freshly created organization be?
Status codes, briefly
- 200 — successful read, update, or custom action.
- 201 — you created a resource. Return it in the body.
- 204 — successful DELETE. Empty body, because there is nothing left to describe.
- 404 — the client asked for a specific resource that doesn't exist.
That last one has an edge people get wrong constantly. A list endpoint that matches nothing returns 200 with an empty array, never 404. GET /organizations?status=nonsense returning 404 is a lie: the collection exists, the filter just matched zero rows. 404 is only for a request that named one thing and that thing is gone.
Consistency beats correctness
If POST /organizations takes description, then POST /projects takes description too. Not desc. Not details. The frontend engineer integrating your API reads one endpoint and extrapolates to the rest — that is not laziness, that is what a consistent interface is for. Every field you rename between resources is a validation error someone has to debug against docs they shouldn't have needed to open.
Same for JSON casing: pick camelCase and never deviate, in both directions. Same for abbreviations: don't. You know what qty means because you wrote the schema; the person integrating at 11pm does not.
If you follow the REST standard, great. If for some reason you can't, follow something and never break it. An API that is consistently unusual is cheaper to integrate than one that is standard in four places and improvised in six.