Designing Endpoints - Conventions That Read Well
Knowing the mental model is one thing; laying out an API somebody else can use without reading a manual is
another. The good news is that REST has a well-worn set of conventions, and they're mostly common sense
once you see why each exists. Follow them and a stranger can guess your endpoints; ignore them and even
your own teammates will be grep-ing the source to find out what /doUserThing does.
This phase is the practical layer: how to name things, what to return, and how to handle the everyday needs - filtering, sorting, and paging - that every real API runs into.
The endpoint cheat sheet
Designing something now? Scan this, then read the section for the part you're unsure about.
| You want to… | Do this |
|---|---|
| Name an endpoint | Use a plural noun: /orders, not /getOrders or /order (§1) |
| Act on one record vs. many | Item /orders/42 vs. collection /orders (§1) |
| Say "it worked" | 200 OK (read/update), 201 Created (new), 204 No Content (delete) (§2) |
| Say "you messed up" | 400 bad request, 401 not logged in, 403 not allowed, 404 not found (§2) |
| Say "we messed up" | 500 server error (§2) |
| Filter / sort / paginate a list | Query params: ?status=open&sort=-created&page=2 (§3) |
1. Name with nouns, and be consistent
A REST URL names a thing, and the HTTP method supplies the action - so the URL should be a noun, not a
verb. The verb is already in the method; repeating it in the path (GET /getOrders) is redundant and
breaks the pattern that makes APIs predictable.
❌ verb-in-URL (don't) ✅ noun + method (do)
GET /getAllOrders GET /orders
POST /createOrder POST /orders
GET /getOrderById?id=42 GET /orders/42
POST /updateOrder PATCH /orders/42
POST /deleteOrder?id=42 DELETE /orders/42
Three conventions make the noun style click:
- Plural for collections. Prefer
/ordersover/order. Then/ordersreads as "the orders" and/orders/42as "order 42" - one consistent rule instead of guessing singular vs. plural per endpoint. - Nest to show ownership.
/users/42/ordersmeans "the orders belonging to user 42." Nest one level for a clear parent-child relationship; resist nesting three or four deep - it gets unwieldy fast, and usually/orders?user=42reads better past one level. - Lowercase, hyphenated, no file extensions.
/blog-posts, not/BlogPostsor/blog_posts.json.
💡 Key point. Consistency beats cleverness. An API where every collection is a plural noun and
every item is /collection/{id} is one a developer can navigate by guessing. Each special-case
exception is a thing they now have to look up.
2. Return status codes that actually mean something
Every HTTP response carries a three-digit status code that tells the caller, at a glance, how it went. The number isn't decoration - clients branch on it. Returning the right one is part of your API's contract, not an afterthought.
📝 Terminology - the families. The first digit tells the whole story: 2xx = it worked, 3xx = go somewhere else (redirects), 4xx = the caller did something wrong, 5xx = the server did. That single digit is enough to know whose problem it is.
Here are the ones you'll reach for constantly:
2xx success
200 OK standard success (a GET that found data, a PATCH that worked)
201 Created a POST made a new resource (return its Location)
204 No Content success, nothing to send back (a DELETE that worked)
4xx the caller's fault
400 Bad Request the request body/params are malformed or invalid
401 Unauthorized you didn't prove who you are (missing/bad credentials)
403 Forbidden we know who you are; you're not allowed to do this
404 Not Found no resource at this URL
409 Conflict the request clashes with current state (e.g. duplicate)
5xx the server's fault
500 Internal Server Error something blew up on our side
⚠️ Gotcha - 401 vs. 403. They feel interchangeable; they're not. 401 Unauthorized actually
means unauthenticated - "I don't know who you are, log in." 403 Forbidden means authenticated but not
permitted - "I know exactly who you are, and you still can't touch this." Sending 403 for a missing
login tells the client to fix the wrong thing.
A delete, done right:
HTTP/1.1
HTTP/1.1
The order was removed, and the server returned 204 No Content - success, with an empty body because
there's nothing meaningful to send back about a thing that no longer exists. The caller reads 204 and
knows the delete worked without having to parse anything.
And an error, done right - note that a good 4xx explains itself in the body:
HTTP/1.1
HTTP/1.1
The server rejected the empty order with 400 and a JSON body naming what was wrong and where. The
status code tells the client's code how to branch; the message tells the human reading the logs what to
fix. Returning 400 with a blank body is technically correct and practically useless.
3. Query params for filtering, sorting, and pagination
When you GET a collection, you rarely want all of it in any order. The convention is to shape the
result with query parameters - the ?key=value pairs after the URL. The path still names the
collection; the query refines which slice you get and how it's arranged.
📝 Terminology - query string. Everything after the ? in a URL is the query string:
?status=open&sort=-created&page=2 is three parameters (status, sort, page) joined by &. They're
for narrowing or shaping a read, not for identifying the resource - that's the path's job.
Three jobs, three families of params:
- Filtering - narrow the set:
?status=open,?author=42,?created_after=2026-01-01. - Sorting - order the set:
?sort=created(ascending) or?sort=-created(a leading-for descending is a common convention). - Pagination - return one page at a time so you don't dump a million rows:
?page=2&per_page=25.
"Give me the second page of open orders, newest first, 25 per page":
? HTTP/1.1
HTTP/1.1
The path /orders named the collection; the query string did the rest - filtered to open, sorted
newest-first, and returned page 2. The server wrapped the list in an envelope with paging info (page,
per_page, total) so the client knows there are 312 matches and can build "page 13 of 13." Returning a
bare array instead leaves the client blind to how much more there is.
⚠️ Gotcha - always paginate list endpoints from day one. It's tempting to return the whole collection
while it's small. Then the table grows, one GET /orders tries to serialize a hundred thousand rows, and
the endpoint times out - for every caller at once. Bolting pagination on later is a breaking change to
everyone using it. Build it in before you need it; a default like per_page=25 costs nothing early and
saves an outage later.
💡 Key point - path identifies, query refines. If a value picks out which resource you mean, it
belongs in the path (/orders/42). If it shapes a read of a collection - filter, sort, page - it
belongs in the query string. Keeping that line clean is most of what makes an API feel coherent.
Recap
- Name with plural nouns (
/orders,/orders/42); the HTTP method is the verb, so never put the action in the URL. - Return meaningful status codes -
200/201/204for success,400/401/403/404for caller errors,500for yours - and explain4xxerrors in the body. 401is "log in";403is "you're logged in but not allowed."- Use query params for filtering (
?status=open), sorting (?sort=-created), and pagination (?page=2) - and paginate list endpoints from the start. - Path identifies the resource; query string refines a read of it.
You can now design endpoints that read cleanly. The last phase steps back and tells you the plain truth: where this style holds up, and where it starts to hurt.
← Phase 1: Resources & Verbs · Guide overview · Phase 3: REST in the Real World →
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 3 questions
1. How should you name a REST endpoint?
2. Which status code says a POST created a new resource?
3. What are query parameters for in a REST API?