API Design Best Practices for Full Stack Web Applications
Why API Design Deserves More Attention Than It Gets
In most projects, the API is an afterthought — something that gets bolted on once the database schema is done and the frontend team starts asking for data. We've seen this pattern cause real pain: inconsistent endpoints, breaking changes that ripple across clients, and documentation that's outdated the moment it's written. Good API design isn't glamorous work, but it pays dividends every time a new developer joins the team or a feature needs to expand.
Here's how we approach it.
Start With Resources, Not Actions
REST is built around resources, and the most common mistake we see is designing endpoints around actions instead. Instead of /getUser or /createOrder, think in nouns: /users and /orders. HTTP already gives you the verbs — GET, POST, PUT, PATCH, DELETE. Let them do their job.
A clean resource hierarchy looks like this:
GET /orders # list orders
POST /orders # create an order
GET /orders/:id # get one order
PATCH /orders/:id # partial update
DELETE /orders/:id # delete
GET /orders/:id/items # nested resourceNesting beyond two levels usually signals that the resource model needs rethinking. If you find yourself writing /users/:id/orders/:orderId/items/:itemId/reviews, it's time to flatten.
Version From Day One
We always prefix APIs with a version segment — /api/v1/ — even on the very first endpoint. It costs nothing upfront and saves enormous pain when you need to introduce breaking changes. Our rule of thumb: anything served by a version is a contract. If you need to change the shape of a response, you bump the version rather than breaking existing consumers.
Header-based versioning (Accept: application/vnd.tccb.v2+json) is cleaner in theory, but URL versioning wins in practice because it's visible in logs, browser history, and documentation links.
Be Consistent With Response Shapes
Nothing erodes trust in an API faster than inconsistency. We pick a response envelope early and stick to it everywhere:
// Success
{
"data": { ... },
"meta": { "page": 1, "total": 84 }
}
// Error
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Email address is required.",
"fields": { "email": "required" }
}
}The data / error split means frontend code can branch cleanly on the presence of either key. Machine-readable code strings on errors are especially valuable — they let the client display localised messages without parsing human-readable text.
Use HTTP Status Codes Correctly
Status codes communicate intent, and abusing them forces clients to parse your body just to know if a request succeeded. The ones we use most:
- 200 OK — successful read or update
- 201 Created — resource was created; include a
Locationheader pointing to the new resource - 204 No Content — successful delete with no body
- 400 Bad Request — client sent invalid data
- 401 Unauthorized — not authenticated
- 403 Forbidden — authenticated but not allowed
- 404 Not Found — resource doesn't exist
- 422 Unprocessable Entity — request was well-formed but failed validation logic
- 429 Too Many Requests — rate limit hit; include
Retry-After - 500 Internal Server Error — something broke on our side
We never return 200 with an error body. If it failed, the status code should say so.
Paginate Everything That Returns a List
An endpoint that returns an unbounded list is a performance incident waiting to happen. We default to cursor-based pagination for anything that could grow large, and fall back to offset pagination only when the client genuinely needs to jump to arbitrary pages (like a data export UI).
GET /orders?cursor=eyJpZCI6MTAwfQ&limit=25
// Response
{
"data": [ ... ],
"meta": {
"next_cursor": "eyJpZCI6MTI1fQ",
"has_more": true
}
}Always document the maximum allowed limit and enforce it server-side. Trusting the client to be reasonable is how you end up with a ?limit=100000 request taking down your database.
Treat Documentation as a First-Class Deliverable
We generate API documentation from the code rather than writing it separately — OpenAPI/Swagger specs work well for this. When the spec lives in the same repository as the implementation, it's far less likely to drift. Pair that with contract testing and you have a feedback loop that catches breakage before it ships.
Every endpoint should document: expected inputs, possible response shapes, all error codes it can return, and any authentication requirements. If you wouldn't be comfortable handing the docs to a junior developer and walking away, they need more work.
Small Decisions, Big Impact
Good API design is mostly about making deliberate choices early and applying them consistently. The patterns above aren't revolutionary — they're the boring fundamentals that experienced teams keep rediscovering after the cost of ignoring them becomes obvious.
If you're starting a new project or inheriting an API that's grown difficult to maintain, we're happy to help you think it through. Get in touch and let's talk about what a well-designed API could look like for your application.