Most REST APIs don't fail because of a technology choice — they fail because of decisions made in the first few weeks that nobody revisits until they're painful to change. Inconsistent URL patterns, no versioning strategy, error responses that look different from one endpoint to the next: none of these break anything on day one. They become expensive months later, once a mobile app is in the field with users who can't be force-updated and a web frontend that's grown to depend on the API's current quirks.
If an API is going to serve more than one client — a web frontend today, a mobile app tomorrow, maybe a partner integration after that — it needs to be designed with that reality in mind from the start, not retrofitted once the second client shows up. Here's how we approach it in practice.
Resource-Based URL Structure
REST's core idea, still worth taking seriously, is that URLs represent resources (nouns), and HTTP methods represent actions on those resources (verbs). GET /projects/42/units reads the units belonging to project 42. POST /projects/42/units creates one. PATCH /units/17 updates it. DELETE /units/17 removes it. This sounds obvious written down, but it's the first thing that erodes under deadline pressure — endpoints like /getUserData or /updateStatus creep in because they're faster to name in the moment.
The discipline pays off because a consistent, predictable URL structure means a new developer — or a new client application — can guess a large portion of the API surface correctly without reading documentation. Nest resources only where the relationship is genuinely hierarchical (a unit belongs to a project), and keep nesting shallow — two levels is usually the practical limit before URLs get unwieldy and you're better off flattening with query parameters or a filtering endpoint instead.
Versioning From Day One
Put a version in the URL from the very first commit — /api/v1/... — even if you're certain v2 is years away. The cost of adding it later, once a mobile app has hardcoded the old paths, is far higher than the cost of typing four extra characters in every route definition now.
URL-based versioning (/api/v1 vs /api/v2) is more explicit and easier to debug, test, and document than header-based versioning, where the version is negotiated through an Accept header. Header-based versioning is technically "more correct" by some REST purists' standards, but in practice it's harder to test with a browser or a quick curl command, harder to route at the infrastructure layer, and easier for a client developer to get wrong silently. For most teams, URL versioning's simplicity outweighs its theoretical impurity.
The more important discipline isn't picking a versioning scheme — it's deciding, in writing, what actually justifies bumping the version number. A breaking change (removing a field, changing a field's type, changing required parameters, changing status codes for existing scenarios) justifies a new version. Adding new optional fields or new endpoints does not — those should be safe to release into the existing version without breaking anyone.
Consistent Pagination
Every list endpoint should paginate the same way. Pick one strategy and apply it everywhere: offset-based pagination (?page=2&per_page=25) is simpler to implement and understand, and fine for most admin panels and typical list views. Cursor-based pagination is worth the added complexity for high-volume, frequently-changing data sets (activity feeds, transaction logs) where offset pagination can skip or duplicate records as new rows are inserted between page loads.
Whichever you choose, return pagination metadata consistently — total count (where feasible), current page, per-page size, and whether a next page exists — in the same shape on every paginated endpoint. A mobile app building infinite scroll needs to rely on that shape being identical whether it's fetching units, customers, or payment records; if pagination metadata is inconsistent across endpoints, every list screen in the mobile app needs special-case handling, which is exactly the kind of hidden cost that shows up as bugs months after launch.
A Consistent Response Envelope and Error Format
Decide on one response shape and never deviate from it, success or failure. A common, sensible pattern:
{
"data": { ... },
"meta": { ... }
}
And for errors, a shape with enough structure that a client can act on it programmatically, not just display raw text:
{
"error": {
"code": "validation_failed",
"message": "The amount field is required.",
"fields": { "amount": ["The amount field is required."] }
}
}
The specific shape matters less than the consistency. A mobile app and a web frontend both need to write one error-handling function, not one per endpoint, and that's only possible if every single endpoint — including the ones added eighteen months from now by a developer who never read the original design doc — returns errors in the same structure. Use HTTP status codes correctly and consistently alongside this (401 for unauthenticated, 403 for unauthorized, 422 for validation failures, 404 for missing resources, 429 for rate limiting) so clients can branch on status code first and parse the body second.
Idempotency for Write Operations
Mobile networks drop connections mid-request more often than people building against Wi-Fi in an office ever experience firsthand. When a client retries a request after a timeout — did the original request succeed and only the response get lost, or did it never arrive at all? — the API needs to handle that ambiguity gracefully, particularly for anything involving money, bookings, or any operation where "did that happen twice" is a real business problem.
The standard pattern is an idempotency key: the client generates a unique key (a UUID) per logical operation and sends it in a header (Idempotency-Key). The server stores the key alongside the result of the first request with that key, and if it sees the same key again, it returns the original stored result instead of processing the operation a second time. This is a small amount of backend work — a table mapping keys to results, with a reasonable expiry — that eliminates an entire class of duplicate-charge and duplicate-booking bugs that are otherwise very hard to reproduce and debug after the fact.
Rate Limiting
Rate limiting protects the API from abusive clients, buggy retry loops, and simple traffic spikes, and it should be treated as a default, not an afterthought bolted on after an incident. Return standard rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After) so well-behaved clients can back off gracefully instead of hammering the endpoint the moment they get a 429. Different endpoint categories usually warrant different limits — authentication endpoints need tighter limits than general read endpoints, given they're a common target for credential-stuffing attempts. This overlaps directly with broader API security practices, which we cover in more depth in securing your SaaS API.
Keeping Documentation in Sync
Documentation that drifts from the actual API behavior is worse than no documentation, because it actively misleads whoever's building against it — usually costing them debugging time trying to figure out why the "documented" behavior doesn't match reality. The sustainable fix is to generate documentation from the code rather than maintaining it by hand in a separate document: OpenAPI/Swagger specs generated from route definitions and Form Request validation rules (Laravel tooling like Scribe or L5-Swagger can do this directly from your existing controllers and requests), so the docs update automatically as the API changes rather than requiring someone to remember to update a wiki page. If documentation requires a manual, separate step that isn't part of the normal development workflow, it will eventually go stale — that's not a discipline problem, it's a predictable outcome of relying on a workflow that isn't enforced by anything.
Backward Compatibility: Designing for Mobile Apps You Can't Force-Update
This is the constraint that makes API design for mobile fundamentally different from API design for a web frontend you fully control. When you ship a change to a Next.js or Vue frontend, every user gets the new version on their next page load — you control both sides of the contract simultaneously. A mobile app is different: once it's published, some meaningful percentage of your users will be running last month's version, last year's version, or an even older version, because they haven't updated the app, their OS doesn't support the latest release, or they simply never opened the app store update prompt. Those old app versions keep calling your API, sometimes for years, and the API has to keep serving them correctly. This constraint matters even more for the app itself — for the tradeoffs involved in shipping and maintaining a mobile client at all, see turning your web app into a mobile app.
Practical rules that keep this manageable:
- Never remove a field from a response that an existing client might depend on — deprecate it, document the deprecation, and remove it only in a new major version.
- Never change a field's type or meaning in place — if a field needs to change from a string to an object, add a new field instead and migrate clients to it over time.
- Add new required parameters only in a new API version, never to an existing one — an old app version has no way to know it needs to start sending a field it was never built to send.
- Keep old API versions running and supported for a defined, published deprecation window, not indefinitely and not cut off abruptly — give app developers and users real time to update.
- Track which app versions are actually hitting each API version in production so the decision to sunset an old version is based on real usage data, not a guess.
REST vs GraphQL: When Each Makes Sense
REST isn't the only option, and it's worth being honest about when GraphQL is genuinely the better fit rather than defaulting to REST out of habit. GraphQL earns its complexity when clients have highly variable data needs — a mobile app screen that needs a lean subset of fields to save bandwidth, next to a web dashboard that needs a much richer nested payload from the same underlying data — and when over-fetching or under-fetching with REST would otherwise mean maintaining a sprawl of slightly different endpoints to serve each client's exact shape.
For most business applications — the kind with fairly well-defined resources, standard CRUD operations, and a REST client library ecosystem (particularly on mobile, where REST tooling remains more mature and better supported by caching layers) — REST's simplicity, cacheability, and universal tooling support make it the more maintainable long-term choice. GraphQL also shifts real complexity onto the backend (resolver performance, query complexity limits, caching strategy) that a well-designed REST API mostly avoids by using standard HTTP caching semantics. Choose GraphQL because your data-fetching patterns genuinely demand its flexibility, not because it's the newer option.
None of this is exotic engineering — it's a small set of decisions made deliberately and consistently instead of by accident, endpoint by endpoint. The API design choices covered here compound directly with the stack decisions covered in Laravel vs Next.js, since the backend framework you choose shapes how naturally these patterns fit. If you're designing an API that needs to support a web app and a mobile app for the long haul, talk to us before the first version ships — the cheapest time to get this right is before anyone is depending on it.