Skip to main content

🚦 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.
Anatomy of an HTTP request and response A request carries a method, URL, headers, and an optional body. The response carries a status code, headers, and a body. Request POST /api/products Content-Type: application/json Authorization: Bearer … { "name": "Headphones",   "price": 129.99 } Response 201 Created Content-Type: application/json Location: /api/products/42 { "id": 42, "name": "Headphones",   "price": 129.99 }
Figure 1 — Method + URL + headers + optional body go out; a status code + headers + body come back.

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:

MethodPurposeBody?Typical success code
GETRead a resourceNo200 OK
POSTCreate a resource (or trigger an action)Yes201 Created
PUTReplace a resource entirelyYes200 OK / 204 No Content
PATCHUpdate part of a resourceYes200 OK
DELETERemove a resourceUsually no204 No Content

Here is the full lifecycle of a single resource, expressed as methods against a REST-style URL:

sequenceDiagram participant C as Client participant S as Server C->>S: POST /api/products (new data) S-->>C: 201 Created + Location header C->>S: GET /api/products/42 S-->>C: 200 OK + product C->>S: PATCH /api/products/42 (price only) S-->>C: 200 OK + updated product C->>S: DELETE /api/products/42 S-->>C: 204 No Content

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.

MethodSafe?Idempotent?Cacheable?
GET
HEAD
PUT
DELETE
POSTRarely
PATCHNot 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

HeaderDirectionWhat it does
Content-TypeBothMedia type of the body, e.g. application/json
AcceptRequestFormats the client can handle in the response
AuthorizationRequestCredentials, e.g. Bearer <token>
Cache-ControlBothHow (and whether) to cache, e.g. max-age=3600
LocationResponseURL of a newly created resource or a redirect target
Set-CookieResponseStores a cookie on the client
ETagResponseA 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:

flowchart TD A[Status code] --> B["1xx — Informational
(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.

CodeMeaningWhen to use it
200 OKSuccess with a bodyA successful GET, or an update that returns data
201 CreatedResource createdA successful POST; add a Location header
204 No ContentSuccess, no bodyA successful DELETE, or an update returning nothing
301 / 308Moved permanentlyResource relocated for good (308 keeps the method)
304 Not ModifiedUse your cacheConditional GET where nothing changed
400 Bad RequestMalformed requestInvalid JSON or missing required fields
401 UnauthorizedNot authenticatedMissing/invalid credentials (really "unauthenticated")
403 ForbiddenNot allowedAuthenticated, but lacks permission
404 Not FoundNo such resourceThe ID or URL doesn't exist
409 ConflictState conflictDuplicate record, concurrent-edit clash
422 UnprocessableSemantic errorWell-formed but fails business rules (validation)
429 Too Many RequestsRate-limitedAdd a Retry-After header
500 Internal Server ErrorUnhandled failureA bug or crash on your side
503 Service UnavailableTemporarily downMaintenance 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/json on JSON responses, and set security headers globally (via middleware, e.g. helmet in Express).
  • Include a machine-readable code plus a human message in every error body.
  • Add Retry-After with 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

  1. Open your browser's DevTools → Network tab, then load a data-driven site (a shop, a dashboard).
  2. Click a few XHR/fetch requests. For each, note the method, the status code, the request's Accept/Authorization headers, and the response's Content-Type and Cache-Control.
  3. 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"}'
  4. 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

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