⚡ TL;DR: Good REST falls out of one idea: model resources as nouns, act on them with HTTP verbs. Pair that with the right status codes, sane pagination, and honest error shapes to build APIs other engineers enjoy using, and know when REST is the wrong tool.
Contents
- Why most APIs are annoying to use
- The one principle everything follows from
- The resource model, in one picture
- Verbs and status codes: the lookup table
- Request and response shapes that scale
- When REST is the wrong tool
- Common mistakes that make APIs painful
- Takeaways
- Where to go next
Why most APIs are annoying to use
You integrate with an API and within ten minutes you're confused. The endpoint to delete a thing is POST /deleteUser. Creating a record returns 200 OK even when validation failed. Listing 50,000 rows dumps all of them in one response. The error body is a raw stack trace. None of this is hard to fix, it's just that nobody decided on the rules up front.
A good HTTP API is predictable: once you've seen three endpoints, you can guess the fourth. That predictability is not an accident, it comes from a small set of conventions that REST gives you for free. This article walks through those conventions: how to name resources, pick verbs and status codes, shape requests and responses, paginate, filter, and report errors, plus when REST is the wrong tool and you should reach for GraphQL or gRPC instead.
📌 Who this is for: Backend engineers designing their first public or internal API, and anyone who has wired up a few endpoints but never stepped back to ask whether the shape is right. You should be comfortable with HTTP basics, if requests and responses feel fuzzy, read How the Web Works first.
The one principle everything follows from
Model your API around resources, the nouns of your system, and let HTTP's verbs and status codes describe what happens to them. The protocol already has the vocabulary; your job is to use it consistently.
Think of a well-organized public library. Every book has a fixed, predictable address: a shelf, a section, a call number. You don't ask the librarian to runBookFetchProcedure, you say "I want the book at this location," and the action (borrow, return, reserve) is a separate, standard gesture. The catalog is the same for every book, so once you understand one, you understand all of them. A REST API is that catalog: stable addresses for things, standard actions on them.
| In the real world | In tech |
|---|---|
| A book's shelf location / call number | A resource URL, /users/42 |
| Borrow, return, reserve (standard gestures) | HTTP verbs, GET, POST, PUT, DELETE |
| The catalog card, identical for every book | A consistent JSON response shape |
| "Not on shelf" vs "no such book" | Status codes, 409 vs 404 |
| Section → shelf → book | Nested resources, /users/{id}/orders |
REST borrows a vocabulary you already understand from a well-run library.
The resource model, in one picture
Before you write a single handler, sketch the resources and how they nest. A collection (/users) holds items, each item has a stable address (/users/{id}), and items can own sub-collections (/users/{id}/orders). That hierarchy is your whole URL design.
Collections, items, and nested sub-collections form a predictable tree.
Notice what's missing: there are no verbs in the path. /users/{id}/orders describes a thing, "the orders belonging to this user", and the HTTP method decides whether you're reading the list (GET), adding one (POST), and so on. The URL is a noun; the method is the verb.
Designing one endpoint, step by step
- Name the resource as a plural noun: It's a collection of things, so use the plural: /orders, not /order or /getOrders. Plurals read naturally both for the list (/orders) and the item (/orders/123).
- Pick the verb from the action: Reading is GET, creating is POST, full replace is PUT, partial update is PATCH, removal is DELETE. The verb never goes in the URL.
- Decide the request shape: GET carries no body, filters go in the query string. POST/PUT/PATCH take a JSON body with only the fields the client controls (never let them set id, createdAt, or owner).
- Choose the success status code: 200 for a successful read or update, 201 Created for a new resource (with a Location header), 204 No Content for a delete with nothing to return.
- Define the response shape: Return the resource (or a list wrapped with pagination metadata). Keep the shape identical to what the matching GET returns, so clients learn it once.
- Map the failure modes to codes: 400 for malformed input, 401/403 for auth, 404 for a missing resource, 409 for a conflict, 422 for validation. Each gets the same structured error body.
Verbs and status codes: the lookup table
Most of REST collapses into one table. Memorize this and you've covered ninety percent of the endpoints you'll ever build. The pattern repeats for every resource in your system, that repetition is exactly what makes the API predictable.
| Operation | Method + Path | Success status |
|---|---|---|
| List users (paginated) | GET /users |
200 OK |
| Get one user | GET /users/{id} |
200 OK / 404
|
| Create a user | POST /users |
201 Created |
| Replace a user | PUT /users/{id} |
200 OK |
| Update some fields | PATCH /users/{id} |
200 OK |
| Delete a user | DELETE /users/{id} |
204 No Content |
The canonical operations on a users collection. Every other resource looks the same.
💡 Idempotency matters: GET, PUT, and DELETE are idempotent, calling them twice has the same effect as calling them once. POST is not, which is why a retried POST can create duplicates. Knowing this tells clients which requests are safe to retry on a flaky network.
Request and response shapes that scale
The hardest part of a list endpoint isn't the happy path, it's everything around the data. A real GET /users needs pagination (don't return 50,000 rows), filtering (let callers narrow results without new endpoints), and a consistent error shape for when things go wrong. Here's a single well-designed endpoint showing all three.
GET /users?status=active&page=2&limit=20
// Request: filters and paging live in the query string, never the path.
// GET /users?status=active&sort=-createdAt&page=2&limit=20
//
// Response: 200 OK
{
"data": [
{ "id": "u_8a31", "name": "Mara Voss", "status": "active", "createdAt": "2026-05-02T10:14:00Z" },
{ "id": "u_8a32", "name": "Theo Lund", "status": "active", "createdAt": "2026-05-02T09:51:00Z" }
],
"pagination": {
"page": 2,
"limit": 20,
"totalItems": 4213,
"totalPages": 211,
"nextPage": "/users?status=active&sort=-createdAt&page=3&limit=20"
}
}
Three things to copy here. The actual records live under a data key, never at the top level, that leaves room to add pagination (and later, things like meta) without breaking clients. Filters and sort are query parameters, so you never invent a new endpoint for "active users sorted by date." And the response hands back a ready-made nextPage link so the client doesn't have to build URLs by hand.
Now the failure case. When validation fails, return a status code that says "you" (a 4xx), not "me" (a 5xx), and a structured body the client can actually parse, not a sentence and definitely not a stack trace.
POST /users, validation failed
// Response: 422 Unprocessable Entity
{
"error": {
"code": "validation_failed",
"message": "One or more fields are invalid.",
"requestId": "req_b7c2f0",
"details": [
{ "field": "email", "issue": "must be a valid email address" },
{ "field": "age", "issue": "must be greater than 0" }
]
}
}
A machine-readable code lets clients branch (if (error.code === "validation_failed")), a human message helps the developer debugging it, a requestId ties the failure to your server logs, and per-field details let a frontend highlight the exact inputs that were wrong. Use this shape for every error in the API, from 400 to 503, so callers write their error handling once.
When REST is the wrong tool
REST is the right default for most resource-shaped, public-facing APIs. But two situations push you elsewhere: clients that need wildly different slices of data (a mobile screen wants 3 fields, a dashboard wants 30), and high-throughput internal service-to-service calls where every millisecond and byte counts.
| Dimension | REST | GraphQL | gRPC |
|---|---|---|---|
| Best for | Public/CRUD APIs | Varied client data needs | Internal service-to-service |
| Data fetching | Fixed per endpoint | Client picks exact fields | Fixed, typed RPC methods |
| Transport / format | HTTP + JSON | HTTP + JSON | HTTP/2 + Protobuf (binary) |
| Caching | Easy (HTTP caching) | Harder (POST queries) | Custom |
| Learning curve | Low | Medium | Medium-high |
Pick the style that matches the consumer and the traffic.
📌 Rule of thumb: Start with REST. Reach for GraphQL when many different clients each need a different shape of the same graph of data and over-/under-fetching is real pain. Reach for gRPC when two of your own services talk to each other at high volume and you want strict typed contracts and a binary wire format.
Common mistakes that make APIs painful
-
Verbs in the URL.
POST /createUser,GET /getUserById,POST /users/123/delete. The method already is the verb, usePOST /users,GET /users/123,DELETE /users/123. -
Returning 200 for errors. A
200 OKwrapping{ "success": false }forces every client to parse the body to know if the call worked, and breaks monitoring that watches status codes. Let the status code tell the truth. - No pagination on list endpoints. Returning every row works in dev with 12 records and falls over in prod with 12 million. Paginate from day one, adding it later is a breaking change.
-
Leaking internals. Stack traces, SQL fragments, ORM class names, and internal IDs in responses are a security and coupling hazard. Return a clean structured error with a
requestId, and keep the gory details in your logs. -
Inconsistent shapes.
datahere,resultsthere,itemssomewhere else; snake_case in one endpoint, camelCase in another. Pick one convention and enforce it everywhere, consistency is the whole product.
Takeaways
REST API design in eight lines
- Model resources as plural nouns; never put verbs in the URL.
- Let HTTP methods be the verbs: GET, POST, PUT, PATCH, DELETE.
- Status codes must tell the truth, 2xx for success, 4xx for the client, 5xx for you.
- Use 201 + Location on create, 204 on delete, 422 for validation.
- Wrap lists in a
datakey withpaginationmetadata, paginate from day one. - Put filters and sorting in the query string, not new endpoints.
- Use one structured error shape everywhere, with a
codeand arequestId. - Default to REST; reach for GraphQL or gRPC only when the consumer demands it.
Where to go next
You now have the shape of a clean API. The next questions are how it changes over time and how you keep it safe, both are their own disciplines.
- API Design: Evolution & Versioning, how to change an API without breaking the clients already depending on it.
- Secure API Design, authentication, authorization, rate limiting, and the inputs you must never trust.
- How the Web Works: HTTP Requests, the request/response fundamentals everything here builds on.
- Follow the full Backend Engineer path to see where API design sits in the bigger picture.
Originally published on TheSimplifiedTech, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.
Top comments (0)