๐ฎ HTTP Methods & Status Codes
If REST is the grammar of web APIs, HTTP methods and status codes are its verbs and its punctuation. A request says what you want done; a status code says exactly how it went. Get these two vocabularies right and your API becomes self-explanatory โ clients know how to ask, and know how to react.
Week 7 · Day 3 (Wednesday: RESTful API Design) · Lecture 2
๐ฏ Learning Objectives
By the end of this lesson, you will be able to:
- Explain the purpose and semantics of GET, POST, PUT, PATCH, and DELETE
- Distinguish PUT (full replace) from PATCH (partial update) with confidence
- Read an HTTP request and response line by line
- Choose the correct status code for success, client-error, and server-error cases
- Return
201 Created,204 No Content, and4xxerrors properly in Express - Design a consistent, informative error-response body
Estimated Time: 65 minutes
Practice: Wire up a full set of route handlers that each return the right status code, and build a status-code decision tree.
In This Lesson
Anatomy of a Request & Response
Every HTTP exchange is a pair of plain-text messages. The client sends a request; the server sends back a response. Once you can read them, nothing about APIs feels mysterious.
Here is a request and its response, annotated:
GET /api/users/123 HTTP/1.1 <- method, path, protocol version
Host: example.com <- which server
Accept: application/json <- "I'd like JSON back, please"
Authorization: Bearer eyJhbGc... <- who I am (stateless auth)
HTTP/1.1 200 OK <- protocol, status code, reason phrase
Content-Type: application/json <- the format of the body
Cache-Control: max-age=60 <- how long this may be cached
{ "id": 123, "name": "Ada Lovelace" } <- the body (a representation)
A request has a method (the verb), a path (the resource), headers (metadata), and an optional body (data you're sending). A response has a status code (how it went), headers, and usually a body. The rest of this lesson is really just: pick the right method for the request, and the right status code for the response.
The HTTP Methods in Depth
๐ The Restaurant Analogy
Picture ordering at a restaurant. GET is reading the menu โ you look, nothing changes. POST is placing a new order โ the kitchen creates something. PUT is saying "replace my whole order with this instead." PATCH is "actually, just hold the onions." DELETE is "cancel my order." The waiter's reply โ "coming right up," "we're out of that," "the kitchen's on fire" โ is the status code.
GET โ retrieve a resource
Safe and idempotent. A GET must never change server state; it only reads. Successful GETs return 200 OK with the resource in the body.
app.get('/api/users/:id', (req, res) => {
const user = getUserById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'Not Found', message: 'No such user' });
}
res.status(200).json(user); // 200 + the representation
});
POST โ create a resource
Not safe, not idempotent. POST submits data to a collection and the server creates a new member with a server-assigned id. Respond 201 Created and set a Location header pointing at the new resource.
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Bad Request', message: 'name and email are required' });
}
if (getUserByEmail(email)) {
return res.status(409).json({ error: 'Conflict', message: 'Email already registered' });
}
const user = createUser({ name, email });
res.status(201)
.location(`/api/users/${user.id}`) // where to find the new resource
.json(user);
});
PUT โ replace a resource
Not safe, but idempotent. PUT sends the complete new representation; the server replaces the resource wholesale. Sending the same PUT twice yields the same final state โ that's the idempotency.
app.put('/api/users/:id', (req, res) => {
const { name, email, role } = req.body;
if (!name || !email) { // PUT needs the FULL resource
return res.status(400).json({ error: 'Bad Request', message: 'Complete representation required' });
}
if (!getUserById(req.params.id)) {
return res.status(404).json({ error: 'Not Found' });
}
const user = replaceUser(req.params.id, { name, email, role });
res.status(200).json(user);
});
PATCH โ partially update a resource
Not safe; not necessarily idempotent. PATCH carries only the fields that change, leaving the rest untouched.
app.patch('/api/users/:id', (req, res) => {
const existing = getUserById(req.params.id);
if (!existing) return res.status(404).json({ error: 'Not Found' });
const user = updateUser(req.params.id, req.body); // merge only sent fields
res.status(200).json(user);
});
DELETE โ remove a resource
Not safe, but idempotent. On success, respond 204 No Content โ the deletion happened and there's nothing left to send back.
app.delete('/api/users/:id', (req, res) => {
if (!getUserById(req.params.id)) {
return res.status(404).json({ error: 'Not Found' });
}
deleteUser(req.params.id);
res.status(204).end(); // 204 = success, empty body
});
๐ก HEAD and OPTIONS, briefly
HEAD is a GET with no response body โ handy for checking whether a resource exists or has changed without downloading it. OPTIONS reports which methods a resource supports and powers CORS preflight requests. You rarely write these by hand; Express and browsers often handle them for you.
PUT vs PATCH โ the distinction that trips everyone up
Both update an existing resource, but they mean different things. PUT replaces; PATCH modifies. Send a PUT with only some fields and, strictly speaking, the missing fields should be wiped to their defaults โ because you declared this to be the resource's complete new state.
Start with this user:
{ "id": 123, "name": "Ada Lovelace", "email": "ada@example.com", "role": "admin" }
| Request body | Resulting resource | |
|---|---|---|
| PUT /users/123 | { "name": "Ada L.", "email": "ada@x.com" } |
role is gone โ PUT replaced the whole thing |
| PATCH /users/123 | { "email": "ada@x.com" } |
only email changed; name and role untouched |
โ ๏ธ Don't send a partial body with PUT
Using PUT for partial updates is the most common REST bug in the wild. If you only want to change one field, use PATCH. Reach for PUT only when the client genuinely holds โ and is sending โ the full resource.
Status Code Families
Status codes are three digits, and the first digit tells you the category. Memorize the five families and you can guess the meaning of a code you've never seen.
๐ก 4xx vs 5xx is a blame line
A 4xx says "your request was wrong โ fix it and try again." A 5xx says "your request was fine, but something broke on our end." Returning 500 for a validation error (which is really the client's fault) sends debuggers hunting in the wrong place. Get the family right.
The Codes You'll Actually Use
There are dozens of status codes, but a REST API leans on about a dozen. Here they are, grouped, with when to reach for each.
2xx โ Success
| Code | Meaning | Use it whenโฆ |
|---|---|---|
| 200 OK | Success, body included | A GET succeeds, or a PUT/PATCH returns the updated resource |
| 201 Created | New resource made | A POST creates something โ add a Location header |
| 204 No Content | Success, empty body | A DELETE succeeds, or an update returns nothing |
3xx โ Redirection
| Code | Meaning | Use it whenโฆ |
|---|---|---|
| 301 Moved Permanently | New permanent URL | A resource's URL changed for good |
| 304 Not Modified | Cache is still fresh | A conditional GET (If-None-Match) matches โ saves bandwidth |
4xx โ Client Error
| Code | Meaning | Use it whenโฆ |
|---|---|---|
| 400 Bad Request | Malformed request | Missing field, invalid JSON, wrong type |
| 401 Unauthorized | Not authenticated | No/invalid credentials โ we don't know who you are |
| 403 Forbidden | Not allowed | Authenticated, but not permitted to do this |
| 404 Not Found | No such resource | The URL points at nothing |
| 409 Conflict | State conflict | Duplicate email, editing a stale version |
| 422 Unprocessable Entity | Semantic/validation error | JSON is valid but fails business rules (age < 18) |
โ ๏ธ 401 vs 403 โ authentication vs authorization
401 Unauthorized means "I don't know who you are" โ you're not logged in, or your token is bad. 403 Forbidden means "I know exactly who you are, and you still can't do this." A logged-in non-admin trying to delete another user gets 403, not 401. (Yes, the name "Unauthorized" is misleading โ it's really about authentication.)
5xx โ Server Error
| Code | Meaning | Use it whenโฆ |
|---|---|---|
| 500 Internal Server Error | Something broke | An uncaught exception โ the catch-all failure |
| 503 Service Unavailable | Temporarily down | Maintenance or overload โ pair with Retry-After |
Two representative handlers โ a validation failure and a conflict:
// 422 โ the JSON parsed fine, but the values break our rules
app.post('/api/users', (req, res) => {
const { email, age } = req.body;
const errors = [];
if (age !== undefined && (age < 18 || age > 120)) {
errors.push('age must be between 18 and 120');
}
if (email && !email.includes('@')) {
errors.push('email format is invalid');
}
if (errors.length) {
return res.status(422).json({
error: 'Unprocessable Entity',
message: 'Validation failed',
details: errors
});
}
// ...create the user
});
// 403 โ authenticated, but not permitted
app.delete('/api/users/:id', (req, res) => {
const me = req.user; // set by auth middleware
if (me.role !== 'admin' && me.id !== req.params.id) {
return res.status(403).json({
error: 'Forbidden',
message: 'You may only delete your own account'
});
}
// ...delete
});
What the client receives (422)
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"error": "Unprocessable Entity",
"message": "Validation failed",
"details": ["age must be between 18 and 120"]
}
Designing Error Responses
A status code tells the client the category of failure; the body should tell it the specifics. Adopt one error shape and use it everywhere โ consistency is what lets a client write a single error handler for your whole API.
// A consistent error envelope, reused across every endpoint
{
"error": "Not Found", // short, machine-friendly title
"message": "User with ID 123 not found", // human-readable explanation
"code": "USER_NOT_FOUND", // optional stable app-level code
"details": [], // optional per-field errors
"timestamp": "2026-07-31T12:34:56.000Z", // when it happened
"path": "/api/users/123" // which endpoint
}
Centralize it with Express error-handling middleware so individual routes stay clean:
// A custom error carrying its own status code
class ApiError extends Error {
constructor(statusCode, message, details = []) {
super(message);
this.statusCode = statusCode;
this.details = details;
}
}
// Routes just throw โ the middleware formats the response
app.get('/api/users/:id', (req, res, next) => {
const user = getUserById(req.params.id);
if (!user) return next(new ApiError(404, `User ${req.params.id} not found`));
res.json(user);
});
// One error handler to shape them all (must have 4 args)
app.use((err, req, res, next) => {
const status = err.statusCode || 500;
if (status === 500) console.error(err); // log the unexpected ones
res.status(status).json({
error: err.name || 'Error',
message: status === 500 ? 'An unexpected error occurred' : err.message,
details: err.details || [],
timestamp: new Date().toISOString(),
path: req.originalUrl
});
});
โ Never leak internals in a 500
In production, a 500 body should say "An unexpected error occurred" โ not a stack trace or SQL string. Log the full detail server-side (where you can see it) and hand the client a safe, generic message. Leaking internals is both a poor experience and a security risk.
Practice & Quiz
๐๏ธ Exercise 1: Pick the status code
Goal: For each scenario, name the single best status code.
- A POST successfully creates a new order.
- A DELETE succeeds and there's nothing to return.
- A request arrives with no auth token on a protected route.
- A logged-in basic user tries to access an admin-only report.
- A POST tries to register an email that already exists.
- The JSON is valid but
ageis-5.
๐ก Hint
Separate "who are you?" (401) from "you're not allowed" (403). Separate "malformed" (400) from "valid JSON, bad values" (422). A duplicate is a state conflict.
โ Solution
1. 201 Created (new resource made)
2. 204 No Content (success, empty body)
3. 401 Unauthorized (not authenticated)
4. 403 Forbidden (authenticated but not permitted)
5. 409 Conflict (duplicate โ conflicts with current state)
6. 422 Unprocessable Entity (valid JSON, fails validation)
๐๏ธ Exercise 2: Complete the handler
Goal: Fill in a POST handler for /api/articles that validates a required title, rejects a duplicate slug, and returns the created article correctly.
app.post('/api/articles', (req, res) => {
const { title, slug } = req.body;
// TODO 1: reject missing title with the right 4xx code
// TODO 2: reject a duplicate slug with the right 4xx code
// TODO 3: create the article and respond with the right 2xx code + Location
});
โ Solution
app.post('/api/articles', (req, res) => {
const { title, slug } = req.body;
if (!title) {
return res.status(400).json({ error: 'Bad Request', message: 'title is required' });
}
if (getArticleBySlug(slug)) {
return res.status(409).json({ error: 'Conflict', message: 'slug already in use' });
}
const article = createArticle({ title, slug });
res.status(201)
.location(`/api/articles/${article.id}`)
.json(article);
});
400 for the missing field, 409 for the duplicate, 201 + Location for the successful create.
๐ฏ Quick Quiz
Question 1: A POST successfully creates a resource. What should the server return?
Question 2: A user is logged in but tries to delete someone else's account. Which code?
Question 3: Which statement about PUT and PATCH is correct?
Best Practices & Pitfalls
โ Do
- Return 201 + Location on create, 204 on delete
- Keep GET safe โ never mutate state in a GET handler
- Use 4xx for client mistakes, 5xx for server failures
- Ship a consistent error body across every endpoint
- Distinguish 401 (who are you?) from 403 (you can't)
โ Don't
- Return
200 OKwith{"error": ...}in the body โ the status must reflect the outcome - Use PUT to change a single field (that's PATCH)
- Send a stack trace or SQL in a
500response - Answer every failure with
400โ reach for 401/403/404/409/422 when they fit - Forget the
Locationheader after a201
โ ๏ธ The "always 200" anti-pattern
// โ Lies to the client โ the request FAILED but the status says OK
res.status(200).json({ success: false, error: 'User not found' });
// โ
The status code IS the outcome
res.status(404).json({ error: 'Not Found', message: 'User not found' });
Monitoring tools, caches, and client libraries all read the status code, not your body. Make it tell the truth.
Summary
๐ Key Takeaways
- An HTTP exchange is a request (method + path + headers + body) and a response (status + headers + body)
- GET reads, POST creates, PUT replaces, PATCH partially updates, DELETE removes
- PUT replaces the whole resource; PATCH changes only the fields you send
- The leading digit names the family: 2xx success, 3xx redirect, 4xx client error, 5xx server error
- Know the workhorses: 200, 201, 204, 400, 401, 403, 404, 409, 422, 500
- Ship a consistent error body and never leak internals in a 500
๐ Additional Resources
- MDN โ HTTP request methods
- MDN โ HTTP response status codes
- restfulapi.net โ HTTP Status Codes
- MDN โ 422 Unprocessable Content
๐ What's Next?
You now speak the verbs and the punctuation of HTTP. Next you'll put them to work at scale in API Endpoint Design โ structuring resource URLs, nesting relationships, adding filtering, sorting, pagination, and versioning so a whole API stays consistent and predictable.
๐ Well done!
Your API can now say precisely what it did and precisely what went wrong โ the difference between an API developers tolerate and one they trust.