I've reviewed several hundred REST APIs over the last decade and a half — at Mayven, portfolio companies, Capital One. The mistakes aren't novel. They cluster. There are four main ones, and each is fixable in the first week if you know to look. Once in production, they're forever — someone depends on them, and you can't change them without breaking that someone.
Here they are, ordered by how badly they bite.
Mistake one: confusing resources with actions.
REST is noun-first. The URL names a thing; the HTTP verb describes what you do to it. GET /orders/123, POST /orders, DELETE /orders/123. Reasonable. The mistake is when your API looks like RPC: POST /cancelOrder, POST /refundOrder, POST /orderActions. URLs with verbs lose REST's value. Cacheability breaks. Generic client libraries can't reason about your endpoints. Permissions get harder to model. Future API consumers — including you — can't predict what's available by reading the URL shape.
The fix is uncomfortable but cheap: name the resource, even when it feels weird. A cancellation is a resource. POST /orders/123/cancellations creates one. GET /orders/123/cancellations lists them. The cancellation has an ID, a timestamp, a reason, an actor. The action becomes data, which you wanted all along, because now you can audit, query, and reverse it without a side table. The same logic applies to refunds, exports, subscription pauses, password resets. Promote them to resources. Your future self will thank you.
Mistake two: pagination as an afterthought.
Every API starts with GET /things returning everything. Six months later, there are 50,000 things, and the dashboard times out. Someone adds ?limit=100 and ?offset=0, ships it, marks the ticket done, and goes home. Three years later, the same dashboard queries offset=4900, taking thirty seconds because the database does a full scan to skip the first 4,900 rows.
Offset pagination is a trap. It looks like it works. It performs worse the deeper you paginate, as the database counts to your offset every time, and it gives inconsistent results when items are inserted or deleted between page requests. The page-three result at 10am isn't the same set at 10:01am. Customer-facing APIs that paginate this way produce bug reports that are nearly impossible to reproduce.
Use cursor pagination from day one. Sort by something stable (created-at + ID is reliable), and return a cursor pointing to the last item in the page. The next request asks for items after that cursor. This is O(log n) regardless of depth, consistent under concurrent writes, and it's the model GitHub, Stripe, and Slack converged on for a reason. The cost on day one is an extra hour of work. The cost on day 800 is rewriting it under fire.
Include a sane default page size (50 is fine, 25 is fine, 1000 is not), enforce a maximum, and never make a client paginate to get a count of total items. If they need a count, give them a separate endpoint that returns it. Don't make them iterate.
Mistake three: status codes as decoration.
HTTP has a reasonable set of status codes. Use them. The pattern I see is everything returns 200 OK with a body that says {"success": false, "error": "..."}, and then the API "documents" that you have to read the body to find out if it failed. This is wrong — your CDN will cache the error, your monitoring tools won't alert on failures, retry middleware won't know to retry, and your logs will say everything succeeded until your customer calls.
The right shape:
A successful read is 200. A successful create is 201 with a Location header pointing to the new resource. A successful delete with no body is 204. Client did something wrong is 4xx — 400 for malformed requests, 401 for unauthenticated, 403 for authenticated-but-not-allowed, 404 for missing, 409 for conflicts with current state, 422 for valid-shape-but-invalid-content, 429 for rate-limited. Your server broke is 5xx. A 5xx should mean "this is our fault and a retry might succeed"; a 4xx should mean "this is your fault and retrying won't help."
Getting this right affects more than the API surface. Every layer of infrastructure between you and your client speaks HTTP status codes. Load balancers, observability platforms, CDNs, retry libraries, alerting rules — all act on the codes. When you collapse everything into 200, you blind every layer of the stack.
Mistake four: no versioning strategy, then a versioning crisis.
Almost no API at v0 needs a version in its URL. Then six months later, someone has a customer running production traffic against it, and you need to make a breaking change. Now you have an unversioned API and a breaking change to make, and the choice is "break the customer" or "support the old behavior forever via clever side logic." Both are bad.
Pick a versioning scheme on day one and write it down. There are three sane options:
URL versioning (/v1/users). Simple, obvious, easy to route at the load balancer. Downside: forces a copy-paste of every endpoint when you go to v2, even ones that didn't change.
Header versioning (Accept: application/vnd.yourcompany.v2+json). Cleaner separation between resource identity and representation. Downside: it's invisible in logs and harder to debug because two requests to the same URL behave differently. Most teams that adopt this wish they'd done URL versioning.
Date-pinning (API-Version: 2026-05-26). Stripe's model. Pin a customer to a date, evolve the API continuously, and run a translation layer that converts the live API back to the date-pinned format. Powerful, expensive, only worth it at API-as-product scale. If you're not Stripe, you're not Stripe. Don't pretend you are.
For most teams, URL versioning is the right answer. It's the cheapest to operate, easiest to reason about, and the cost of copy-paste between v1 and v2 is real but bounded. The point is: pick something on day one. Don't wait for the crisis.
Two smaller things worth flagging.
Idempotency keys for any non-GET endpoint that costs money or mutates external state. Stripe popularized this and it's correct. The client generates a UUID, sends it in a header, and the server promises that repeated requests with the same key return the same result without re-executing the operation. This makes retry-safe clients possible. Bake it in from the start.
Error response shape consistency. Every error should look the same: a status code, an error code (machine-readable, stable), and a message (human-readable, can change). If different endpoints return errors in three different shapes, every client integrates you three times. Pick one shape on day one and enforce it in the framework.
That's it. Entire books are written about this. They're mostly correct, and 80% of what you need is in this article. Get these four things right and your API will outlast the team that built it.