🚦 HTTP Methods, Headers, and Status Codes
The request/response cycle told you that the browser and server talk. This lesson teaches you what they actually say: the verbs that name the action, the headers that carry the metadata, and the three-digit codes that report the result. These three pieces are the working vocabulary of every backend developer.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Choose the correct HTTP method (GET, POST, PUT, PATCH, DELETE, and friends) for any operation, and explain safety and idempotency
- Read and set HTTP headers for content type, caching, authentication, and security
- Return meaningful status codes from all five classes (1xx–5xx) instead of a blanket 200
- Implement the same endpoint correctly in Express, Flask, and PHP
Estimated Time: 35–45 minutes • Difficulty: Beginner–Intermediate
Hands-on: Inspect a real API's methods, headers, and codes with your browser's DevTools and curl.
In This Lesson
The Three Building Blocks
Every HTTP exchange is a short, structured message. Strip away the details and each request answers three questions, and each response answers one more:
- What action? → the method (GET, POST, DELETE…)
- On which thing? → the URL (the resource)
- With what extra details? → the headers (and sometimes a body)
- And how did it go? → the response's status code
💡 The language analogy. HTTP is a tiny language. Methods are its verbs ("GET" = "please give me"). URLs are its nouns (/users/123= "the user with ID 123"). Headers are the adjectives and adverbs ("Accept: application/json" = "reply in JSON, please"). And status codes are the short replies ("200" = "done", "404" = "couldn't find it"). Fluency means knowing both the words and how they fit together.
HTTP Methods (Verbs)
A method declares the operation you want to perform on a resource. Five carry almost all of the weight in day-to-day API work:
| Method | Purpose | Body? | Typical success code |
|---|---|---|---|
| GET | Read a resource | No | 200 OK |
| POST | Create a resource (or trigger an action) | Yes | 201 Created |
| PUT | Replace a resource entirely | Yes | 200 OK / 204 No Content |
| PATCH | Update part of a resource | Yes | 200 OK |
| DELETE | Remove a resource | Usually no | 204 No Content |
Here is the full lifecycle of a single resource, expressed as methods against a REST-style URL:
PUT vs PATCH — the most common mix-up
PUT replaces the whole resource. Send the complete representation; any field you omit is treated as removed or reset. PATCH updates only the fields you send. If you only want to change a price, PATCH is the right tool:
PATCH /api/products/42
Content-Type: application/json
{ "price": 139.99 }
📖 Less common but worth knowing
HEAD — like GET but returns only headers, no body. Great for checking whether a resource exists or changed without downloading it.
OPTIONS — asks which methods a resource supports. The browser sends it automatically as a CORS "preflight" before certain cross-origin requests.
TRACE and CONNECT — diagnostic/tunneling methods you rarely write by hand; TRACE is usually disabled for security.
Safety & Idempotency
Two properties decide how a method may be used, cached, and retried. They sound academic but they cause real bugs when ignored.
💡 Definitions
Safe: the request does not change server state. It only reads. GET and HEAD are safe.
Idempotent: making the request once or many times has the same end result. GET, PUT, and DELETE are idempotent; POST is not.
| Method | Safe? | Idempotent? | Cacheable? |
|---|---|---|---|
| GET | ✅ | ✅ | ✅ |
| HEAD | ✅ | ✅ | ✅ |
| PUT | ❌ | ✅ | ❌ |
| DELETE | ❌ | ✅ | ❌ |
| POST | ❌ | ❌ | Rarely |
| PATCH | ❌ | Not necessarily | ❌ |
⚠️ Why this matters in practice
Because POST is not idempotent, a user who double-clicks "Buy" can create two orders. Because DELETE is idempotent, a client that retries after a dropped connection can safely re-send it — the second DELETE just returns 404 or 204, not an error. Never hide a state change behind GET: a search-engine crawler or a link-preview bot following your links could silently delete data.
HTTP Headers
Headers are key–value metadata attached to requests and responses. They never carry the "main" payload — that's the body — but they tell each side how to interpret and handle it.
Headers you will use constantly
| Header | Direction | What it does |
|---|---|---|
Content-Type | Both | Media type of the body, e.g. application/json |
Accept | Request | Formats the client can handle in the response |
Authorization | Request | Credentials, e.g. Bearer <token> |
Cache-Control | Both | How (and whether) to cache, e.g. max-age=3600 |
Location | Response | URL of a newly created resource or a redirect target |
Set-Cookie | Response | Stores a cookie on the client |
ETag | Response | A version fingerprint for conditional caching |
Security headers — set these on every response
A handful of response headers harden your app against common browser-side attacks. They cost nothing and stop a whole class of bugs:
Content-Security-Policy: default-src 'self'— controls which sources may load scripts, styles, and images; your strongest defense against XSS.Strict-Transport-Security: max-age=31536000; includeSubDomains— forces HTTPS for future visits.X-Content-Type-Options: nosniff— stops the browser from guessing (and mis-guessing) content types.X-Frame-Options: DENY— prevents your pages being embedded in a hostile<iframe>(clickjacking).
📖 A note on cookies
When you set a session cookie, always add HttpOnly (JavaScript can't read it), Secure (HTTPS only), and SameSite=Lax or Strict (limits cross-site sending, blunting CSRF): Set-Cookie: sessionId=abc123; Path=/; HttpOnly; Secure; SameSite=Lax
Status Codes
The response status code is a three-digit number grouped into five classes. The first digit tells you the category at a glance:
(rarely seen)"] A --> C["2xx — Success"] A --> D["3xx — Redirection"] A --> E["4xx — Client error
(you sent something wrong)"] A --> F["5xx — Server error
(the server broke)"]
The single most useful skill here is telling 4xx from 5xx: a 4xx means the client must fix the request; a 5xx means the server must fix itself. Below are the codes you will reach for daily.
| Code | Meaning | When to use it |
|---|---|---|
| 200 OK | Success with a body | A successful GET, or an update that returns data |
| 201 Created | Resource created | A successful POST; add a Location header |
| 204 No Content | Success, no body | A successful DELETE, or an update returning nothing |
| 301 / 308 | Moved permanently | Resource relocated for good (308 keeps the method) |
| 304 Not Modified | Use your cache | Conditional GET where nothing changed |
| 400 Bad Request | Malformed request | Invalid JSON or missing required fields |
| 401 Unauthorized | Not authenticated | Missing/invalid credentials (really "unauthenticated") |
| 403 Forbidden | Not allowed | Authenticated, but lacks permission |
| 404 Not Found | No such resource | The ID or URL doesn't exist |
| 409 Conflict | State conflict | Duplicate record, concurrent-edit clash |
| 422 Unprocessable | Semantic error | Well-formed but fails business rules (validation) |
| 429 Too Many Requests | Rate-limited | Add a Retry-After header |
| 500 Internal Server Error | Unhandled failure | A bug or crash on your side |
| 503 Service Unavailable | Temporarily down | Maintenance or overload; add Retry-After |
⚠️ The "200 with an error message" anti-pattern
Returning 200 OK with a body like { "success": false } defeats every tool that relies on status codes — caches, monitoring dashboards, retry logic, and client libraries all think the call worked. Let the status code carry the outcome; use the body for the details.
Worked Example: The Same Endpoint in Three Languages
Let's build a "create a product" endpoint that puts all three building blocks to work: it accepts a POST, reads and sets headers, validates the body, and returns the right status code for each outcome. Notice how the idea is identical across stacks.
Node.js (Express)
import express from 'express';
const app = express();
app.use(express.json()); // parse JSON request bodies
app.post('/api/products', (req, res) => {
const { name, price } = req.body;
// 400: the request itself is malformed
if (!name || price == null) {
return res.status(400).json({
code: 'INVALID_INPUT',
message: 'Both "name" and "price" are required.'
});
}
// 403: authenticated, but not allowed
if (!req.user?.isAdmin) {
return res.status(403).json({
code: 'FORBIDDEN',
message: 'Only administrators can create products.'
});
}
// 409: conflicts with existing state
if (db.findByName(name)) {
return res.status(409).json({
code: 'PRODUCT_EXISTS',
message: 'A product with that name already exists.'
});
}
const product = db.create({ name, price });
// 201 + Location header + security header
res.set('X-Content-Type-Options', 'nosniff')
.location(`/api/products/${product.id}`)
.status(201)
.json(product);
});
app.listen(3000);
Python (Flask)
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.post('/api/products')
def create_product():
data = request.get_json(silent=True) or {}
name, price = data.get('name'), data.get('price')
if not name or price is None:
return jsonify(code='INVALID_INPUT',
message='Both "name" and "price" are required.'), 400
if not current_user_is_admin():
return jsonify(code='FORBIDDEN',
message='Only administrators can create products.'), 403
if db.find_by_name(name):
return jsonify(code='PRODUCT_EXISTS',
message='A product with that name already exists.'), 409
product = db.create(name=name, price=price)
resp = jsonify(product)
resp.status_code = 201
resp.headers['Location'] = f"/api/products/{product['id']}"
resp.headers['X-Content-Type-Options'] = 'nosniff'
return resp
PHP
<?php
header('Content-Type: application/json');
header('X-Content-Type-Options: nosniff');
$data = json_decode(file_get_contents('php://input'), true) ?? [];
$name = $data['name'] ?? null;
$price = $data['price'] ?? null;
if (!$name || $price === null) {
http_response_code(400);
echo json_encode(['code' => 'INVALID_INPUT',
'message' => 'Both "name" and "price" are required.']);
exit;
}
if (!currentUserIsAdmin()) {
http_response_code(403);
echo json_encode(['code' => 'FORBIDDEN',
'message' => 'Only administrators can create products.']);
exit;
}
if ($db->findByName($name)) {
http_response_code(409);
echo json_encode(['code' => 'PRODUCT_EXISTS',
'message' => 'A product with that name already exists.']);
exit;
}
$product = $db->create($name, $price);
http_response_code(201);
header("Location: /api/products/{$product['id']}");
echo json_encode($product);
A successful call returns:
HTTP/1.1 201 Created
Location: /api/products/42
Content-Type: application/json
{ "id": 42, "name": "Wireless Headphones", "price": 129.99 }
Best Practices
✅ Do
- Match the method to the intent: read → GET, create → POST, replace → PUT, tweak → PATCH, remove → DELETE.
- Return the most specific status code you can — 201 for creates, 204 for empty successes, 404 vs 403 vs 401 distinctly.
- Send
Content-Type: application/jsonon JSON responses, and set security headers globally (via middleware, e.g.helmetin Express). - Include a machine-readable
codeplus a human message in every error body. - Add
Retry-Afterwith 429 and 503 so clients know when to come back.
⚠️ Don't
- Don't perform writes with GET, or hide "delete" behind a link a crawler can follow.
- Don't return 200 for failures — let the status code tell the truth.
- Don't use
Access-Control-Allow-Origin: *for authenticated endpoints. - Don't leak stack traces or SQL in 500 responses; log details server-side, return a generic message.
Hands-on Exercise
🏋️ Read a real API's HTTP conversation
Objective: See methods, headers, and status codes in the wild — no code required.
Instructions
- Open your browser's DevTools → Network tab, then load a data-driven site (a shop, a dashboard).
- Click a few XHR/fetch requests. For each, note the method, the status code, the request's
Accept/Authorizationheaders, and the response'sContent-TypeandCache-Control. - From a terminal, run a request and read the raw headers:
curl -i https://httpbin.org/status/404 curl -i -X POST https://httpbin.org/post \ -H "Content-Type: application/json" \ -d '{"name":"test"}' - Write down one example each of a 2xx, a 3xx (try a URL that redirects), and a 4xx you triggered, and explain what each means.
💡 Hint
The -i flag in curl includes the response headers and status line in the output. To follow redirects and watch each hop, add -L and -v. In DevTools, the "Status" column shows the code and the "Headers" sub-tab splits request vs response headers.
✅ Example answer
2xx: GET /api/products returned 200 OK with Content-Type: application/json and Cache-Control: max-age=60 — a cacheable read. 3xx: GET http://github.com returned 301 with Location: https://github.com/ — permanent HTTP→HTTPS redirect. 4xx: curl https://httpbin.org/status/404 returned 404 Not Found — the resource doesn't exist, so the fix is on the client's side.
🎯 Quick Quiz
Question 1: A client sends the same DELETE request twice because the first response was lost. Why is this safe?
Question 2: A user is logged in but tries to delete another user's post and isn't allowed. Which status code fits best?
Question 3: You want to change only a product's price without touching its other fields. Which method is correct?
Summary & Quiz
🎉 Key Takeaways
- Methods name the action: GET reads, POST creates, PUT replaces, PATCH tweaks, DELETE removes.
- Safety (no state change) and idempotency (repeatable) govern caching and safe retries.
- Headers carry metadata — content type, caching, auth, and the security headers you should always set.
- Status codes report the outcome; the first digit gives the class, and 4xx (client) vs 5xx (server) is the key split.
- The same rules apply in Express, Flask, and PHP — different syntax, identical semantics.
📚 Further Reading
- MDN — HTTP request methods
- MDN — HTTP response status codes
- MDN — HTTP headers reference
- RFC 9110 — HTTP Semantics (the current spec)
🚀 What's Next?
Now that you can speak HTTP fluently, the next lesson steps up to architecture: RESTful API Design Principles shows how to organize these methods and URLs into a clean, predictable, resource-oriented API.
🎉 Well done!
You now know exactly what the browser and server are saying to each other. Let's design APIs worth talking to.