The first API I shipped professionally had a endpoint called /getUsers. I was proud of it. It returned a JSON array, it worked in Postman, and my tech lead had signed off on the pull request with a single thumb emoji. Three months later, a mobile team in a different time zone was cursing that endpoint in a Slack thread I was quietly tagged into. The payload was enormous — every user field we had, including ones we'd added speculatively and never cleaned up — and there was no pagination. Just a wall of JSON arriving all at once, like someone had tipped a filing cabinet through a window.
I have been thinking about that filing cabinet a lot lately.
Over the years I have designed, inherited, argued over, and occasionally broken more REST APIs than I care to count. Some were internal tools that only three people ever touched. Some were public-facing contracts that outlived the teams that built them. And somewhere along the way I accumulated a private list of REST API design mistakes I made — the kind you don't find in tutorials because tutorials show you the happy path, the clean example, the /users/{id} that always returns exactly what you need.
Real APIs are messier. And the mistakes tend to compound.
When the URL Became a Sentence
The first category of damage I did was to the URLs themselves. REST is supposed to give you a uniform interface, a set of conventions so obvious that a new developer can read a route and understand what it does. I violated that contract repeatedly, and always with good intentions.
The /getUsers problem is the obvious one — verbs in URLs, treating HTTP routes like function names. But I did subtler things too. I nested resources four and five levels deep because it felt logical: /organizations/{orgId}/teams/{teamId}/members/{memberId}/permissions. It is logical, in the way that a sentence diagram is logical. Nobody wants to read a sentence diagram. What I eventually learned — after watching mobile developers hard-code those URLs and then panic every time we restructured the hierarchy — is that deep nesting creates brittle coupling. A flat or shallowly-nested structure, with relationships expressed through query parameters or response links, ages far better.
I also spent years being inconsistent with pluralization. Some of my resources were /user, some were /users. I had a period where I thought singular made more semantic sense for singleton resources. Maybe it does. But inconsistency is its own kind of bug — it forces every consumer to remember an exception, and exceptions are where mistakes live.
The Versioning Problem I Kept Deferring
Here is a thing I told myself on at least four separate projects: we'll add versioning when we need it.
We always needed it before I thought we would.
The first time a breaking change hit an unversioned API I owned, the fix was ugly. We added a query parameter — ?v=2 — as a stopgap while we argued about whether to put the version in the URL path or the Accept header. That argument lasted two weeks. In the meantime, the old behavior and the new behavior coexisted in the same codebase through a conditional branch that I am certain is still there, commented with my name.
Versioning is one of those decisions that feels premature until it is catastrophically late. The REST API design mistakes I made around versioning weren't about choosing the wrong strategy — URL path versus header is a genuinely open debate — they were about treating versioning as an optimization rather than a foundation. You build the foundation first. You don't pour it after the house is standing.
What I do now, even on internal APIs that I am certain will never go public: /v1/ in the path from day one. It costs nothing. It buys enormous amounts of future flexibility. And it signals to every developer who reads the route that this is a contract, not a draft.
Error Responses That Told You Nothing
If I could go back and fix one class of mistake across every API I have ever touched, it would be the error responses.
For a long time, my error handling looked like this: something goes wrong, return a 400 or a 500, maybe a JSON body with { "error": "Something went wrong" }. Done. I thought I was being RESTful because I was using the right status codes. I was not being useful.
A status code tells you the category of the problem. It does not tell you which field failed validation, which permission was missing, or which downstream service timed out. Without that specificity, every error becomes a debugging expedition. I have watched frontend developers spend half a day on a bug that a single additional field in the error response would have resolved in ten minutes.
The RFC 7807 "Problem Details" spec exists precisely for this reason — a standardized shape for error bodies that includes a type, a title, a status, a detail, and an optional instance. I ignored it for years because I hadn't heard of it. When I finally read it, I felt the particular frustration of someone who has been reinventing a wheel badly.
Good error responses are documentation. They are the API talking to the developer at the exact moment the developer most needs help. Treating them as an afterthought is one of the REST API design mistakes I made that I genuinely regret.
The Payload Problem: Giving Too Much, Then Too Little
My /getUsers endpoint gave you everything. Later APIs I designed gave you almost nothing — I had overcorrected, afraid of bloated payloads, and started returning minimal objects that required three follow-up requests to assemble anything useful.
Both are failure modes. The first creates bandwidth and parsing problems. The second creates chattiness — a pattern where a single user action triggers a cascade of API calls, each one adding latency, each one a new opportunity for failure.
The honest answer is that neither extreme is wrong in isolation; they're wrong for the context. A list endpoint probably shouldn't return every nested relationship. A detail endpoint probably should. What I failed to do, consistently, was think about the actual consumption patterns before I designed the response shape. I designed APIs from the database outward — here are my tables, here are my fields, here are your endpoints — rather than from the client inward — here is what you need to render this screen, let's work backward.
GraphQL exists partly as a reaction to this failure mode, and I understand the appeal. But you don't need GraphQL to fix it. You need to sit with the people consuming your API before you write the first route, and ask them what a successful response actually looks like.
What the Filing Cabinet Taught Me
I think about APIs differently now than I did when I shipped that /getUsers endpoint. Not because I read a definitive book or attended the right conference, but because I have spent enough time on the receiving end of my own decisions to understand what they cost.
The REST API design mistakes I made were rarely dramatic. Nobody's production system caught fire because I pluralized inconsistently. But they accumulated — into debugging sessions, into migration headaches, into that Slack thread where a developer I'd never met was trying to understand why I'd thought it was reasonable to return four thousand users in a single unpadded response.
The filing cabinet through the window. I think about it because it captures something true about how bad API design feels to the person on the other end: not malicious, not even careless exactly, just someone who didn't stop to consider what it would be like to catch the thing.
The question I try to ask now, before I finalize any route or any response shape, is a simple one: if I were the developer receiving this, would I feel respected? It is a softer standard than any spec, and maybe that's why it works. Specs describe correctness. Respect describes craft.
I'm still not sure I always get it right. But I'm asking the question, which is more than I was doing when I named an endpoint /getUsers and waited for the thumb emoji.