REST API design has almost no enforcement mechanism — nothing stops you from calling a resource /getUserData instead of GET /users/:id — which is exactly why so many "RESTful" APIs aren't, and why consuming them is harder than it should be.
REST API design is the practice of modeling your API around resources (nouns) manipulated through standard HTTP methods (verbs), using status codes, headers, and URL structure the way HTTP itself was designed to be used. The value isn't philosophical purity — it's predictability. A well-designed REST API is guessable: once you know one endpoint's shape, you can correctly guess the rest.
Why REST Design Matters (and When to Skip It)
Consistent REST design pays off the moment more than one person or client consumes your API — predictable resource naming and status codes mean less documentation reading and fewer integration bugs. It also unlocks HTTP's built-in caching semantics, which GraphQL and RPC-style APIs have to reimplement manually.
Skip strict REST conventions for pure RPC-style internal APIs where resource modeling doesn't fit the operation (like a POST /calculate-tax action), or when GraphQL/tRPC better fits your client's actual data-shape needs.
Getting Started with REST API Design
Resources are nouns, HTTP methods are verbs — the combination should read naturally:
GET /users → list users
POST /users → create a user
GET /users/:id → get one user
PATCH /users/:id → partially update a user
PUT /users/:id → fully replace a user
DELETE /users/:id → delete a user
GET /users/:id/posts → list a user's posts (nested resource)
app.get("/users/:id", async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!user) return res.status(404).json({ error: "User not found" });
res.json(user);
});
Core REST Design Concepts Every Developer Should Know
Status codes carry real meaning — use them precisely. 200 for success, 201 for successful creation (with a Location header pointing to the new resource), 204 for success with no body (like a DELETE), 400 for client input errors, 401 for missing/invalid auth, 403 for authenticated-but-forbidden, 404 for missing resources, 409 for conflicts (like a duplicate unique field), 422 for semantically invalid input that parsed fine, 500 for server errors.
Pagination should be explicit and consistent. Cursor-based pagination scales better than offset-based for large or frequently-changing datasets:
GET /users?cursor=eyJpZCI6MTAwfQ&limit=20
{
"data": [ /* ... */ ],
"nextCursor": "eyJpZCI6MTIwfQ",
"hasMore": true
}
Versioning prevents breaking existing clients. URL-based (/v1/users) is simplest and most visible; header-based (Accept: application/vnd.api+json;version=1) is cleaner but easier for consumers to miss. Pick one and apply it consistently from the start — retrofitting versioning onto an unversioned API is painful.
Error responses need a consistent shape, not just a status code:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Email is required",
"field": "email"
}
}
Common REST Design Mistakes and How to Fix Them
Mistake 1: verbs in URLs. /getUser, /createUser, /deleteUser/:id duplicate what the HTTP method already expresses. Fix: use nouns for resources and let the HTTP method carry the verb: GET /users/:id, POST /users, DELETE /users/:id.
Mistake 2: returning 200 for everything, including errors. This forces clients to parse the response body just to know if a request succeeded, defeating the purpose of having status codes at all. Fix: return accurate status codes and let clients branch on them before parsing the body.
Mistake 3: deeply nested resource URLs. /users/:id/posts/:postId/comments/:commentId/replies/:replyId becomes unwieldy fast. Fix: flatten to top-level resources with filters where the nesting doesn't add real value: GET /replies?commentId=:commentId.
When Should You Use REST Instead of GraphQL or tRPC?
Use REST for public APIs (where HTTP caching, wide client compatibility, and simplicity matter), webhook receivers, and services with a single, well-understood client. Reach for GraphQL when multiple clients need different data shapes, or tRPC when both client and server are TypeScript and you want end-to-end type safety without a separate schema language.
REST API Design in Production
Document with OpenAPI/Swagger from the start — generated from code via decorators (NestJS) or JSDoc annotations, it keeps docs from drifting out of sync with the actual implementation. Also add rate limiting and consistent request logging early; both are far easier to bake in from day one than retrofit once real traffic exists.
Before adding a new endpoint, ask whether it fits the noun+verb pattern cleanly — if it doesn't, that's often a sign the resource model itself needs rethinking, not just the URL.