Lesson 1 of 6
HTTP & REST APIs
Design REST APIs that make sense: pick methods by semantics, return the right status codes, and stay stateless with clean versioning and pagination.
① Watch
Speed
📖 Read this walkthrough — every command, and why
HTTP & REST APIs — the status code is the contract
Mental model: a web API is a conversation over HTTP. REST gives that conversation a
grammar — your data are nouns you expose as resources (URLs like /todos and /todos/2),
and the HTTP methods are the verbs that act on them. The server's reply is not just a
body: the status code carries the outcome, and the client is meant to read that code
before it reads anything else. Every request/response below is captured verbatim from a
live HTTP server hitting a real /todos resource.
Resources (nouns) and methods (verbs)
Resource is the thing: /todos the collection
/todos/2 one item in it
Method is the action on it: GET read (safe, doesn't change state)
POST create (adds a new resource)
PUT replace (full update of an item)
PATCH modify (partial update of an item)
DELETE remove
You don't name actions in the URL (no /getTodos, no /createTodo). The noun is the URL;
the verb is the method.
Status code families — the first digit tells you who's responsible
2xx success the request worked
3xx redirect / cached go look elsewhere, or you already have it (e.g. 304)
4xx client error the caller got it wrong (bad input, missing resource, no auth)
5xx server error the server faulted; the caller's request was fine
A client that only reads the body and ignores the code is trusting prose over the
contract. The code is the contract.
1 · Read the collection
curl -i http://localhost:3000/todos
GET /todos -> 200 body=[{"id":1,"title":"write tests","done":false}]
200 OK. GET reads; the body is the current list as JSON. GET is safe (it changes
nothing) and idempotent (calling it again returns the same thing, side-effect-free).
2 · Create an item — 201 Created + Location
curl -i -X POST http://localhost:3000/todos \
-H 'Content-Type: application/json' \
-d '{"title":"ship it"}'
POST /todos (valid) -> 201 Created Location=/todos/2 body={"id":2,"title":"ship it","done":false}
Not 200 — 201 Created, the code that says a new resource now exists. The Location
header points at where it lives: /todos/2. That's the creation idiom: 201 + Location.
The client didn't choose the id 2; the server assigned it and told the client where
to find it. Note the Content-Type: application/json request header — that's how the
server knows the body is JSON, not a form or plain text.
3 · Reject bad input — 400 (the client's fault)
curl -i -X POST http://localhost:3000/todos \
-H 'Content-Type: application/json' \
-d '{}'
POST /todos (no title)-> 400 Bad Request body={"error":"title is required"}
Empty body, no title. 400 Bad Request — a 4xx, so the caller got it wrong. The body
explains ({"error":"title is required"}) but the status alone already tells an
automated client this is a validation failure, not a server bug and not a missing
resource. Don't retry a 400 unchanged; the same input fails the same way.
4 · Read the created item — it's really there
curl -i http://localhost:3000/todos/2
GET /todos/2 -> 200 body={"id":2,"title":"ship it","done":false}
200 with the exact todo we created. The write in step 2 stuck: a later, independent
GET sees it. HTTP is stateless — each request carries its own context and the server
remembers nothing about "you" between calls — but your data persist behind the API, so
write-then-read works across separate requests.
5 · Ask for something that was never created — 404
curl -i http://localhost:3000/todos/999
GET /todos/999 -> 404 Not Found body={"error":"not found"}
Same URL shape as /todos/2, different outcome, different code. 404 Not Found means the
resource doesn't exist. This is the key distinction learners blur: 400 = you sent bad
input; 404 = your input was fine but there's nothing at that address. The number tells
the client exactly what happened without parsing the body.
Idempotency — safe to retry?
A method is idempotent if making the same call N times leaves the server in the same
state as making it once. This matters because networks drop responses and clients retry.
GET idempotent read; retrying is harmless
PUT idempotent "set item 2 to this" — same result every time
DELETE idempotent deleting an already-deleted item still ends "gone"
POST NOT idempotent — each POST /todos creates a new row. Retry a POST whose
response you never got and you may create a duplicate (id=3, id=4...).
That's exactly why creation returns 201 + Location: the server owns the identity, and
if you want safe-to-retry creates you design around it (idempotency keys, or PUT to a
client-chosen id).
Statelessness
The server keeps no per-client session between requests. Everything it needs — the
target resource in the URL, the method, headers like Content-Type, the JSON body, and
(for protected routes) an auth token — travels with each request. Persistence lives in
the datastore behind the API, not in server memory tied to a connection. This is what
lets any server instance handle any request and lets the system scale horizontally.
JSON bodies + content type
Both sides agree to speak JSON. Requests that carry a body declare it with
Content-Type: application/json (step 2), and responses come back as JSON too. The
body is the payload; the status code and headers are the envelope that says how to
interpret it.
Notes
- The status code is the contract. 200 read ok, 201 created (with Location), 400 your
input, 404 no such resource. Automated clients branch on the code, not the prose.
- 201 vs 200 on create: use 201 so the caller knows a resource was born, and include
Location so they can fetch it without guessing the id.
- 400 vs 404 is the most-missed distinction: bad input (400) is the client's request
being malformed; missing resource (404) is a well-formed request for something that
isn't there. Returning 404 for bad input (or 400 for a missing id) breaks the contract.
- GET/PUT/DELETE are idempotent; POST is not. Only POST creates duplicates on blind
retry — design creates with that in mind.
- GET should be safe: never mutate state on a GET. Caches, crawlers, and prefetchers
assume GET has no side effects.
- Versioning: once other people depend on your JSON shape, changing it breaks them.
Version the API up front — a URL prefix like /v1/todos or an Accept header — so you can
ship /v2 without breaking /v1 callers.
- Pagination: GET /todos returned two rows here, but a real collection can be millions.
Don't return everything — page it (e.g. GET /todos?limit=20&offset=40, or cursor-based
with a next token) and let the client walk the collection.
Verify
# Round-trip the real contract in five requests. Watch the status line each time.
curl -i http://localhost:3000/todos # 200, list
curl -i -X POST http://localhost:3000/todos \
-H 'Content-Type: application/json' -d '{"title":"ship it"}' # 201, Location: /todos/2
curl -i -X POST http://localhost:3000/todos \
-H 'Content-Type: application/json' -d '{}' # 400, title is required
curl -i http://localhost:3000/todos/2 # 200, the created item
curl -i http://localhost:3000/todos/999 # 404, not found
# Pass check: the codes read 200, 201, 400, 200, 404 — and the 201 carries Location: /todos/2.