APIer/04 · RESTful API design
CHAPTER 04 · URLs, CRUD, error format

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.

§01

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. /posts is the collection and /posts/42 is 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 /BlogPosts and /blogposts are 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 .php or .jsp ending 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.
URL clinic — pick a broken URL on the left to see the diagnosis
/getUser?id=1
Diagnosis
  • The verb get is in the URL
  • The identity of the resource sits in a query parameter
Rewritten
GET/users/1

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.

§02

Mapping CRUD onto HTTP

The URLs are settled. Now attach the actions. This table covers most of the endpoints you will ever design.

RequestIn plain wordsOn successIdempotent?
GET /postsGive me the list of posts200✓ — also safe
POST /postsAdd a new post201✕ — sending it twice makes two posts
GET /posts/42Give me post 42200✓ — also safe
PUT /posts/42Replace the whole of post 42 with what I send200✓ — ten identical writes give one result
PATCH /posts/42Change only these fields of post 42200Not promised
DELETE /posts/42Delete post 42204✓ — see §03
GET /posts/42/commentsThe comments under post 42200✓ — also safe
POST /posts/42/commentsAdd a comment to post 42201

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.

§03

Three complete exchanges

Create, update, delete — real messages on both sides. The middle one is the part of this chapter to read twice.

Exchange 1Creating a post
Request
1POST /posts HTTP/1.1
2Content-Type: application/json
3Authorization: Bearer <token>
4
5{
6 "title": "My first post",
7 "body": "Hello, API world!"
8}
Response
1HTTP/1.1 201 Created
2Location: /posts/43
3Content-Type: application/json
4
5{
6 "id": 43,
7 "title": "My first post",
8 "body": "Hello, API world!",
9 "authorId": 1,
10 "createdAt": "2026-07-01T09:30:00Z"
11}
Three things to notice. The status is 201, not 200, because a resource that did not exist now does. The Location header gives the URL of that new resource. The body echoes the resource in full, because id and createdAt were generated by the server and the client has no other way to learn them.
Exchange 2Updating — where PUT and PATCH part ways

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.

PUT · only title in the body
1PUT /posts/43 HTTP/1.1
2Content-Type: application/json
3
4{ "title": "Just changing the title" }
PATCH · only title in the body
1PATCH /posts/43 HTTP/1.1
2Content-Type: application/json
3
4{ "title": "Just changing the title" }
Response to the PUT
1HTTP/1.1 200 OK
2Content-Type: application/json
3
4{
5 "id": 43,
6 "title": "Just changing the title"
7}
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".
Response to the PATCH
1HTTP/1.1 200 OK
2Content-Type: application/json
3
4{
5 "id": 43,
6 "title": "Just changing the title",
7 "body": "Hello, API world!",
8 "authorId": 1,
9 "createdAt": "2026-07-01T09:30:00Z"
10}
Fields you did not mention are kept. PATCH sends a description of the change, and the server applies it to what is already there.

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.

Exchange 3Deleting, and deleting twice
The first DELETE
1DELETE /posts/43 HTTP/1.1
2Authorization: Bearer <token>
Response
1HTTP/1.1 204 No Content
The delete succeeded and there is nothing to report, so there is no body at all. A 204 response never has one.
The same request again — post 43 is no longer there
1HTTP/1.1 404 Not Found
2Content-Type: application/problem+json
3
4{
5 "type": "https://api.example.com/problems/not-found",
6 "title": "Resource not found",
7 "status": 404,
8 "detail": "Post 43 does not exist."
9}
404 says there is nothing at this URL now. If your server keeps a record of deleted posts, 410 Gone is the more precise answer: it also says the resource existed and was removed on purpose, which lets clients drop cached copies and search engines remove the URL. Only use 410 when you actually know that. The body format is explained in §05.

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.

§04

Choosing a status code

Eleven codes cover almost every answer a REST API needs to give. Read the cards, then work through the scenes.

201 Created

A new resource exists now. Send a Location header with its URL.

202 Accepted

The request was accepted and the work is queued. Nothing exists at a new URL yet.

204 No Content

Success, and there is no body to send. The response has no body at all.

400 Bad Request

The request is malformed — for example the JSON does not parse.

401 Unauthorized

Not authenticated, despite the name. The response must carry WWW-Authenticate.

403 Forbidden

Authenticated, but not permitted to do this. Sending the same credentials again will not help.

404 Not Found

Nothing at this URL now. It makes no claim about whether anything was ever here.

409 Conflict

The request conflicts with the current state — a duplicate value in a unique column, for example.

410 Gone

The resource existed and was deliberately removed. Use it only when you know that.

422 Unprocessable

The syntax parses and the content fails validation. The usual answer for a failed check.

429 Too Many Requests

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.

Status code decisions — read the situation, pick the codeScene 1 / 10 · 0 correct
A client sends POST /posts with a valid new article. The server stored it and is about to send the article back with its new id.
§05

Reporting errors

A wrong status code makes every client work harder. First an anti-pattern, then the standardized shape: RFC 9457.

Anti-pattern · every error dressed up as 200
1HTTP/1.1 200 OK
2Content-Type: application/json
3
4{
5 "success": false,
6 "errCode": 10086,
7 "errMsg": "bad parameter"
8}
Three separate problems. First, 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.

A validation failure in Problem Details form
1HTTP/1.1 422 Unprocessable Content
2Content-Type: application/problem+json
3
4{
5 "type": "https://api.example.com/problems/validation-error",
6 "title": "The request body failed validation",
7 "status": 422,
8 "detail": "The email field is not a valid email address.",
9 "instance": "/users",
10 "errors": [
11 { "field": "email", "message": "Expected the form name@example.com" }
12 ]
13}
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.

§06

Your first API design, on one page

Every decision in this chapter, collected into one table: the blog API.

MethodPathSuccessCommon failuresWhat it does
POST/users201409 · 422Register (409 if the username is taken, 422 if the email is invalid)
GET/users/{id}200404Read a user profile
GET/posts200List posts (filtering and pagination in chapter 05)
POST/posts201400 · 401 · 422Publish a post; returns Location and the new resource
GET/posts/{id}200404 · 410One post (410 if it was deliberately removed)
PUT/posts/{id}200401 · 403 · 404 · 422Replace the whole post — send the complete object
PATCH/posts/{id}200401 · 403 · 404 · 422Change part of a post — send only the fields you want changed
DELETE/posts/{id}204401 · 403 · 404Delete a post; no response body
GET/posts/{id}/comments200404The comments on one post
POST/posts/{id}/comments201401 · 404 · 422Add 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.

§07

Practice

Three tasks: fix broken URLs, reproduce the field loss caused by PUT, and design an endpoint table on your own.

§08

Chapter quiz

Eight questions, all of them decisions you will make in real work. Answer them all correctly to mark the chapter complete.

QUESTION 01 / 8

Which design is the best way to ask for the list of all movies?

QUESTION 02 / 8

What is the standard request for adding a comment to post 42?

QUESTION 03 / 8

POST /posts created the post. What is the most correct response?

QUESTION 04 / 8

You want to change only the title of post 43 and keep every other field. Which request do you send?

QUESTION 05 / 8

A registration endpoint receives JSON that is syntactically valid, but the email field contains "hello". Which status code states the problem most precisely?

QUESTION 06 / 8

RFC 9457 Problem Details says the Content-Type of an error response should be ____. (Write the full media type.)

QUESTION 07 / 8

DELETE /posts/43 returns 204 the first time and 404 the second time. Is DELETE still idempotent?

QUESTION 08 / 8

What should be done to the URL /users/1/posts/2/comments/3/replies/4?

What to take away from this chapter
  • Paths name things, not actions: /posts for the collection, /posts/42 for 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 Location header 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 (with WWW-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 with Retry-After for 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+json is 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.