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 into four main issues, all fixable in the project's first week if you know to look. Once they're in production, they're permanent — someone depends on them, and changing them breaks things.

Here they are, ordered by severity.

Mistake one: confusing resources with actions.

REST is noun-first, as Roy Fielding's 2000 dissertation, Chapter 5 established. The URL names a thing; the HTTP verb describes what you do to it. GET /orders/123, POST /orders, DELETE /orders/123. The mistake is when your API starts looking like RPC: POST /cancelOrder, POST /refundOrder, POST /orderActions. URLs with verbs lose REST's benefits. Cacheability breaks. Generic client libraries can't reason about your endpoints. Permissions get harder because they're scoped to actions, not resources. Future API consumers — including you — can't predict availability by reading the URL shape.

The fix is uncomfortable but cheap: name the resource, even if 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, timestamp, reason, actor. The action becomes data, which you wanted all along — 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 scans to skip the first 4,900 rows.

Offset pagination is a trap. It looks like it works. It performs worse the deeper you paginate because the database counts to your offset every time. It gives inconsistent results when items are inserted or deleted between page requests. The page-three result at 10am isn't the same 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 on 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 all converged on. The cost on day one is one 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 total count. If they need a count, give them a separate endpoint.

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 saying {"success": false, "error": "..."}, and then the API "documents" that you have to read the body to find out if it failed. This is wrong on multiple levels — your CDN will cache the error, your monitoring tools won't alert on failures, retry middleware in client libraries 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 errors are 4xx400 for malformed requests, 401 for unauthenticated, 403 for authenticated-but-not-allowed, 404 for missing, 409 for conflicts, 422 for valid-shape-but-invalid-content, 429 for rate-limited. Server errors are 5xx. A 5xx means "our fault, retry might succeed"; a 4xx means "your fault, retrying won't help."

Getting this right impacts more than the API surface. Every infrastructure layer 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, a customer runs 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 in the same endpoint 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 end up wishing 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. There are entire books written about this. The books are mostly correct, and 80% of what you actually need is in this article. Get these four things right and your API will outlast the team that built it.