Common REST API Architecture Mistakes (and How to Fix Them)
After auditing many codebases, the same API mistakes show up again and again: schema exposure, no versioning, inconsistent errors, missing pagination and idempotency. Here is each one and its fix.
After years of building and auditing APIs, certain mistakes appear with striking regularity. They are not about forgetting syntax, they are architectural choices that seem reasonable at first and become painful at scale. Here are the nine that come up most often in audits, each with the signal that you have it and the fix.
1. Exposing the database schema directly
Returning raw database rows couples every client to your internal schema. Renaming a column or splitting a table then becomes a breaking API change, and internal fields leak into public responses. Put a serialization layer (API Resources, DTOs) between the data and the response, and treat that layer as your published contract.
// Leaks every column, including internal ones
return response()->json($user);
// Controlled, stable contract
class UserResource extends JsonResource {
public function toArray(Request $request): array {
return [
'id' => $this->id,
'name' => $this->name,
'created_at' => $this->created_at->toISOString(),
];
}
}2. Skipping versioning
Shipping an API without a version means every breaking change is a coordinated crisis. Add /v1 to the routes from day one, even for an internal API. It costs nothing up front and lets you run the old and new shapes side by side while consumers migrate on their own schedule.
3. Inconsistent error responses
When each endpoint invents its own error shape, clients cannot handle failures generically and every integration writes bespoke parsing. Adopt one format for the whole API. RFC 9457 problem details is the current standard, served as application/problem+json.
// Each endpoint does its own thing
{ "msg": "not found" }
{ "error_code": 404, "message": "User does not exist" }
// One shape everywhere (RFC 9457)
{
"type": "https://api.example.com/errors/not-found",
"title": "Resource not found",
"status": 404,
"detail": "User with id 42 does not exist.",
"instance": "/v1/users/42"
}4. Blocking the request cycle with heavy work
Report generation, email sending, third-party calls, image processing: none of these should block an HTTP response. The request holds a worker, times out under load, and the client has no way to retry safely. Push the work to a queue, return 202 with a job ID, and let the client poll a status endpoint or receive a webhook when it completes.
5. List endpoints without pagination
A list endpoint that returns everything works fine with a hundred rows and takes down the server at a hundred thousand. Always paginate, with a default page size (20 to 50) and a hard maximum (100). Use offset pagination for small stable datasets and admin screens; use cursor pagination for large or fast-changing lists and infinite scroll, so rows are not skipped or repeated when data shifts between requests.
6. No idempotency on writes
A client sends a POST, the network times out, the client retries, and now there are two orders. Accept an Idempotency-Key header, store the first response keyed by that value, and return the stored response for any repeat with the same key. The retry then produces one record, not two.
POST /v1/orders
Idempotency-Key: 8e03978e-40d5-43e8-bc93-6894a57f9324
Content-Type: application/json
{ "customer_id": 42, "amount": 1990 }7. No rate limiting on public endpoints
Without rate limiting, one misbehaving client or a scraper degrades the service for everyone, and there is no back pressure against brute-force attempts on auth endpoints. Apply a limiter per API key and per IP, return 429 with a Retry-After header, and set tighter limits on expensive or sensitive routes.
8. Secrets in query parameters
Tokens and API keys in the URL end up in server logs, proxy logs, browser history and Referer headers. Send credentials in the Authorization header, and keep anything sensitive out of the query string and the path.
9. No API documentation
An API without a documented contract forces every consumer to read your source or guess. Generate an OpenAPI description from the code so it cannot drift from the implementation, and publish it. Most frameworks do this with a single package.
Recap
| Mistake | Signal you have it | Fix |
|---|---|---|
| Schema exposure | A DB migration breaks a client | Resources or DTOs as the contract |
| No versioning | Every change needs a coordinated release | /v1 prefix from day one |
| Inconsistent errors | Each integration parses errors differently | RFC 9457 problem details everywhere |
| Blocking work | Timeouts under load, slow endpoints | Queue plus job ID plus webhook or poll |
| No pagination | One endpoint returns thousands of rows | Default and max page size, cursor for large lists |
| No idempotency | Retries create duplicate records | Idempotency-Key header, stored response |
| No rate limiting | One client degrades the whole service | Per-key and per-IP limiter, 429 plus Retry-After |
| Secrets in the URL | Tokens visible in logs and history | Authorization header only |
| No documentation | Consumers read your source code | OpenAPI generated from the code |
None of these are hard to fix once you have named them. The expensive part is retrofitting them into an API that already has clients, which is exactly why they belong in the first version.
FAQ
- What is the most common API architecture mistake?
- Returning database rows straight from the controller. It feels efficient, but it couples every client to your schema, so a routine migration becomes a breaking change. A serialization layer between the data and the response is the single highest-value habit.
- Do I need to version an internal API?
- Yes, even if it is only /v1. Internal clients still break when the contract changes, and a version prefix costs nothing to add up front. It lets you run the old and new shape side by side while consumers migrate.
- Offset or cursor pagination?
- Offset is fine for small, stable datasets and admin screens where users jump to a page. Cursor pagination is correct for large or frequently changing lists and for infinite scroll, because it does not skip or repeat rows when data shifts between requests.
- How do I make a POST endpoint idempotent?
- Accept an Idempotency-Key header, store the first response keyed by that value, and return the stored response for any repeat with the same key. A retried request after a network timeout then produces one record, not two.
- RFC 7807 or RFC 9457 for error responses?
- RFC 9457 is the current standard; it obsoletes and replaces RFC 7807 with the same application/problem+json shape and a few clarifications. New APIs should cite 9457. Existing 7807 responses do not need to change.
Most of these mistakes are decisions, not accidents, and each one is cheap to get right at the start and expensive to fix later. When you design the first version of an API, walk this list once before you ship it.
Need help with this topic? REST API Design
Discover this service →