Skip to main content

๐Ÿ“ฎ 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, and 4xx errors 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.

sequenceDiagram participant C as Client participant S as Server C->>S: GET /api/users/123 (method, headers) Note right of C: Accept: application/json S->>C: 200 OK (status, headers, body) Note left of S: Content-Type: application/json

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 bodyResulting 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.

The five HTTP status code families grouped by leading digit 1xx Informational โ€” "still working on it" (rare in APIs) 2xx Success โ€” the request worked (200, 201, 204) 3xx Redirection โ€” "look elsewhere" (301, 304) 4xx Client error โ€” YOU messed up (400, 401, 403, 404, 409, 422) 5xx Server error โ€” the SERVER messed up (500, 503)
The leading digit is the category. The crucial distinction: 4xx blames the client's request, 5xx blames the server.

๐Ÿ’ก 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

CodeMeaningUse it whenโ€ฆ
200 OKSuccess, body includedA GET succeeds, or a PUT/PATCH returns the updated resource
201 CreatedNew resource madeA POST creates something โ€” add a Location header
204 No ContentSuccess, empty bodyA DELETE succeeds, or an update returns nothing

3xx โ€” Redirection

CodeMeaningUse it whenโ€ฆ
301 Moved PermanentlyNew permanent URLA resource's URL changed for good
304 Not ModifiedCache is still freshA conditional GET (If-None-Match) matches โ€” saves bandwidth

4xx โ€” Client Error

CodeMeaningUse it whenโ€ฆ
400 Bad RequestMalformed requestMissing field, invalid JSON, wrong type
401 UnauthorizedNot authenticatedNo/invalid credentials โ€” we don't know who you are
403 ForbiddenNot allowedAuthenticated, but not permitted to do this
404 Not FoundNo such resourceThe URL points at nothing
409 ConflictState conflictDuplicate email, editing a stale version
422 Unprocessable EntitySemantic/validation errorJSON 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

CodeMeaningUse it whenโ€ฆ
500 Internal Server ErrorSomething brokeAn uncaught exception โ€” the catch-all failure
503 Service UnavailableTemporarily downMaintenance 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.

  1. A POST successfully creates a new order.
  2. A DELETE succeeds and there's nothing to return.
  3. A request arrives with no auth token on a protected route.
  4. A logged-in basic user tries to access an admin-only report.
  5. A POST tries to register an email that already exists.
  6. The JSON is valid but age is -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 OK with {"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 500 response
  • Answer every failure with 400 โ€” reach for 401/403/404/409/422 when they fit
  • Forget the Location header after a 201

โš ๏ธ 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

๐Ÿš€ 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.