Designing a RESTful API
In the first three chapters you read other people's designs. In this one you make the decisions: one blog API, from the first URL to a finished table of endpoints.
Naming URLs
A URL is the part of an API that is hardest to change later, because clients store it. Five rules, then a clinic where you diagnose broken ones.
The task you have been given
Your team is building a blog product. People can register, publish articles, and comment on them. That gives you three kinds of resources — users, posts, comments. A backend developer asks you the first question: what do the endpoints look like?
By the end of this chapter you will have a full table of endpoints for it. The same blog stays with you for the rest of the course: chapter 08 writes a GraphQL schema over the same data, and the final chapter uses it to compare the two approaches.
- Rule 1The path names a thing, not an action.
/posts, not/getPosts. The method already carries the verb, so writing it again in the path says the same thing twice. - Rule 2Use a plural noun for a collection.
/postsis the collection and/posts/42is one item in it. Plural is a widely used convention, not a rule in any specification. Some APIs use the singular and work fine. What actually matters is that one API does not mix the two. - Rule 3Pick one spelling style and use it everywhere. Lowercase words joined by hyphens is the common choice:
/blog-posts. Paths are case sensitive, so/BlogPostsand/blogpostsare two different URLs. Mixing styles inside one API produces addresses that look the same and are not. - Rule 4Keep the implementation out of the path. A
.phpor.jspending ties the URL to the language you happen to use today. Change the language and every saved link, bookmark, and integration breaks. - Rule 5One level of nesting is enough.
collection/id/collection, for example/posts/42/comments, states that comments belong to a post. Going deeper writes a hierarchy into the URL that may change later. For anything deeper, filter the top-level collection instead:GET /replies?commentId=3.
- The verb get is in the URL
- The identity of the resource sits in a query parameter
The method already says the action, so the path only needs the noun. User 1 is a resource, so its identity belongs in the path. Query parameters are for filtering, sorting, pagination, and choosing fields — not for identifying which resource you mean.
What goes in the path, what goes in the query
The path says which resource. Query parameters say how you want it: filtering ?status=published, sorting ?sort=-created_at, pagination ?page=2&limit=20, and field selection ?fields=id,title. A query parameter never identifies which resource you mean — that is what /posts/42 is for.
Pagination has one trade-off worth knowing now. ?offset=40&limit=20 is simple, but the window moves when rows are added or removed while a user is reading. In a list sorted newest first, one new post inserted before the reader asks for page 3 pushes everything down by one, so the last item of page 2 appears again at the top of page 3. Cursor pagination sends the position of the last item you received instead — ?after=eyJpZCI6NjB9 — so inserts elsewhere in the list do not shift it. Chapter 05 builds both.
Mapping CRUD onto HTTP
The URLs are settled. Now attach the actions. This table covers most of the endpoints you will ever design.
| Request | In plain words | On success | Idempotent? |
|---|---|---|---|
GET /posts | Give me the list of posts | 200 | ✓ — also safe |
POST /posts | Add a new post | 201 | ✕ — sending it twice makes two posts |
GET /posts/42 | Give me post 42 | 200 | ✓ — also safe |
PUT /posts/42 | Replace the whole of post 42 with what I send | 200 | ✓ — ten identical writes give one result |
PATCH /posts/42 | Change only these fields of post 42 | 200 | Not promised |
DELETE /posts/42 | Delete post 42 | 204 | ✓ — see §03 |
GET /posts/42/comments | The comments under post 42 | 200 | ✓ — also safe |
POST /posts/42/comments | Add a comment to post 42 | 201 | ✕ |
Two words from chapter 02 are doing the work in the last column. Safe means the request is not meant to change anything on the server. Idempotent means sending the same request once or many times leaves the server in the same state.
Combinations that are not in the table are usually a mistake
POST /posts/42 — POST to a single resource — has no agreed meaning, so a reader cannot tell what it does. Avoid it unless you document exactly what it does. And GET must never change data: it is a safe method, so browsers prefetch it and crawlers follow it without asking. The data loss described in the clinic in §01 came from exactly this.
Three complete exchanges
Create, update, delete — real messages on both sides. The middle one is the part of this chapter to read twice.
id and createdAt were generated by the server and the client has no other way to learn them.There are two ways to update, and choosing the wrong one deletes data. Post 43 currently has five fields. Both requests below send the same body, containing only title. Read the two responses side by side.
body, authorId, and createdAt are no longer in the resource. This is what PUT is defined to do: the resource at this URL now equals the representation you sent. A field you left out means absent, not "leave it as it was".The most common way beginners lose data
Sending half an object with PUT. To change one field, use PATCH. To use PUT, read the current resource first and send it back complete, with your one change applied. This difference is also why the two methods differ on idempotence: PUT states the final content, so repeating it gives the same result, while PATCH states a change, and a change such as "increase the view count by 1" adds 2 when it runs twice. The second task in §07 reproduces the field loss against a real API — run it once and you will not forget it.
The second DELETE returns 404 — how is DELETE still idempotent?
Because idempotence is defined by the state of the server, not by the status code. After one delete and after ten, the server is in the same state: post 43 does not exist. The status code is only the reply to that particular request. The full list: GET, PUT, and DELETE are idempotent, POST is not, and PATCH is not promised to be.
Choosing a status code
Eleven codes cover almost every answer a REST API needs to give. Read the cards, then work through the scenes.
A new resource exists now. Send a Location header with its URL.
The request was accepted and the work is queued. Nothing exists at a new URL yet.
Success, and there is no body to send. The response has no body at all.
The request is malformed — for example the JSON does not parse.
Not authenticated, despite the name. The response must carry WWW-Authenticate.
Authenticated, but not permitted to do this. Sending the same credentials again will not help.
Nothing at this URL now. It makes no claim about whether anything was ever here.
The request conflicts with the current state — a duplicate value in a unique column, for example.
The resource existed and was deliberately removed. Use it only when you know that.
The syntax parses and the content fails validation. The usual answer for a failed check.
Too many requests in too short a time. Add Retry-After. Chapter 05 covers rate limits.
400 or 422 for a failed validation?
Both are defensible, and real APIs are split. 422 first appeared in the WebDAV specification (RFC 4918) and was later moved into the core HTTP semantics by RFC 9110, which renamed it Unprocessable Content. It is more specific than 400, because it separates "I could not read your request" from "I read it and one value is invalid". Many APIs still return 400 for both. Pick one and use it consistently across your whole API — a client that cannot predict which code it gets has to handle both anyway. This course uses 422 for validation failures.
Reporting errors
A wrong status code makes every client work harder. First an anti-pattern, then the standardized shape: RFC 9457.
res.ok is true, so every client has to open the body before it knows whether the call worked. Second, caches and proxies between the two sides may store this failure as a successful response and serve it again. Third, errCode: 10086 is private vocabulary — every team that integrates with you has to learn it from scratch.The standardized alternative is RFC 9457 Problem Details (published in 2023, replacing RFC 7807). It defines five members: type, a URI identifying the kind of problem; title, a short summary of that kind; status, the HTTP status code; detail, an explanation of this one occurrence; and instance, a URI for this occurrence. You may add your own members alongside them.
errors is an extension member — the specification expects you to add your own. Note the media type: application/problem+json. A client that sees it knows the body follows this structure without reading your documentation first.Problem Details is standardized, not universal
Plenty of well-run APIs report errors in their own JSON shape and always will. RFC 9457 is worth reaching for because someone has already made the decisions and clients may already handle it. What matters more is the part that is not negotiable: return a status code that matches what happened, say which field or which rule failed, and use the same shape everywhere in one API.
Your first API design, on one page
Every decision in this chapter, collected into one table: the blog API.
| Method | Path | Success | Common failures | What it does |
|---|---|---|---|---|
| POST | /users | 201 | 409 · 422 | Register (409 if the username is taken, 422 if the email is invalid) |
| GET | /users/{id} | 200 | 404 | Read a user profile |
| GET | /posts | 200 | — | List posts (filtering and pagination in chapter 05) |
| POST | /posts | 201 | 400 · 401 · 422 | Publish a post; returns Location and the new resource |
| GET | /posts/{id} | 200 | 404 · 410 | One post (410 if it was deliberately removed) |
| PUT | /posts/{id} | 200 | 401 · 403 · 404 · 422 | Replace the whole post — send the complete object |
| PATCH | /posts/{id} | 200 | 401 · 403 · 404 · 422 | Change part of a post — send only the fields you want changed |
| DELETE | /posts/{id} | 204 | 401 · 403 · 404 | Delete a post; no response body |
| GET | /posts/{id}/comments | 200 | 404 | The comments on one post |
| POST | /posts/{id}/comments | 201 | 401 · 404 · 422 | Add a comment |
This table is more useful than it looks
Method, path, success code, failure codes. The backend implements from it, the frontend calls from it, and the tests are written from it. It is the first draft of an API document, and agreeing on it early means nobody has to guess. Chapter 05 introduces the machine-readable version: OpenAPI, one YAML file that documentation, mock servers, and generated clients all come from.
Practice
Three tasks: fix broken URLs, reproduce the field loss caused by PUT, and design an endpoint table on your own.
Chapter quiz
Eight questions, all of them decisions you will make in real work. Answer them all correctly to mark the chapter complete.
Which design is the best way to ask for the list of all movies?
What is the standard request for adding a comment to post 42?
POST /posts created the post. What is the most correct response?
You want to change only the title of post 43 and keep every other field. Which request do you send?
A registration endpoint receives JSON that is syntactically valid, but the email field contains "hello". Which status code states the problem most precisely?
RFC 9457 Problem Details says the Content-Type of an error response should be ____. (Write the full media type.)
DELETE /posts/43 returns 204 the first time and 404 the second time. Is DELETE still idempotent?
What should be done to the URL /users/1/posts/2/comments/3/replies/4?
- Paths name things, not actions:
/postsfor the collection,/posts/42for one item, and one level of nesting such as/posts/42/comments. Plural collections are a convention — being consistent inside one API matters more than the choice itself. - The path says which resource; query parameters say how you want it — filtering, sorting, pagination, and field selection. A query parameter never identifies a resource.
- PUT replaces the whole representation and is idempotent: a field you leave out means absent, not unchanged. PATCH describes a change and is not idempotent in general. To change one field, use PATCH.
- Status codes are how the response states what happened: 201 with a
Locationheader for a created resource, 202 when the work is queued, 204 for success with no body, 400 for a malformed request, 401 for not authenticated (withWWW-Authenticate), 403 for not permitted, 404 for not found, 410 for deliberately removed, 409 for a state conflict, 422 for content that fails validation, and 429 withRetry-Afterfor too many requests. - Do not dress errors up as 200. Return the status code that matches what happened, and use one error shape across the whole API. RFC 9457
application/problem+jsonis the standardized one; a documented format of your own also works. - Idempotence is about the state of the server, not the status code — a second DELETE answering 404 is still idempotent.
- A table of method, path, and status codes is the first draft of an API document. Chapter 05 turns it into OpenAPI, its machine-readable form.