Hasina Razafintsalama

Hasina RAZAFINTSALAMA

← Back to Blog
Architecture

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.

2026-04-01·11 min

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.

php
// 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.

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.

json
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

MistakeSignal you have itFix
Schema exposureA DB migration breaks a clientResources or DTOs as the contract
No versioningEvery change needs a coordinated release/v1 prefix from day one
Inconsistent errorsEach integration parses errors differentlyRFC 9457 problem details everywhere
Blocking workTimeouts under load, slow endpointsQueue plus job ID plus webhook or poll
No paginationOne endpoint returns thousands of rowsDefault and max page size, cursor for large lists
No idempotencyRetries create duplicate recordsIdempotency-Key header, stored response
No rate limitingOne client degrades the whole servicePer-key and per-IP limiter, 429 plus Retry-After
Secrets in the URLTokens visible in logs and historyAuthorization header only
No documentationConsumers read your source codeOpenAPI 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