Networking Interview Prep
Networking for Web Developers

REST API & HTTP in Practice

REST API & HTTP in Practice

LinkedIn Hook

Everyone says they build REST APIs.

Most don't.

They build HTTP APIs and call them REST. The difference matters — especially in interviews where "what makes an API RESTful?" is a guaranteed question.

Beyond REST principles, the headers you send with every request carry more weight than most developers realize:

  • The difference between Authorization: Bearer and Authorization: Basic
  • Why ETag can eliminate 90% of bandwidth for repeated requests
  • What Cache-Control: no-store does that no-cache doesn't
  • Why cookies and tokens are solving the same problem differently

Lesson 8.2 of my Networking Interview Prep series covers all of it — REST principles, practical headers, cookie vs token auth, and caching mechanics.

Read the full lesson → [link]

#REST #API #WebDevelopment #InterviewPrep #NetworkingFundamentals #BackendDevelopment


REST API & HTTP in Practice thumbnail


What You'll Learn

  • What REST actually is and the 6 constraints that define it
  • How to design URLs, methods, and status codes correctly
  • The headers that every API developer needs to know cold
  • Cookies vs tokens — how both implement stateful auth on a stateless protocol
  • HTTP caching mechanics: Cache-Control, ETag, Last-Modified, and 304 Not Modified
  • What a HATEOAS response looks like (and whether you need it)

The Waiter Script Analogy

A REST API is not just an HTTP endpoint that returns JSON. REST is a set of rules — like a script for a professional waiter.

An unscripted waiter might bring you a bill before you finish eating, forget what you ordered unless you carry a card, or bring random side dishes unbidden. A scripted waiter follows a contract: take the order in a specific format, return items in a predictable format, never need to remember your past visits, and always let you know what you can order next.

REST is that script for client-server communication. The rules exist so any client can talk to any server that follows them, without out-of-band knowledge.


What REST Actually Is

REST stands for Representational State Transfer. It is an architectural style defined by Roy Fielding in his 2000 PhD dissertation, not a specification or standard.

An API is RESTful only if it follows all 6 constraints:

1. Client-Server Separation

Client and server are independent. The client does not care how data is stored. The server does not care how data is displayed. Either can be replaced or upgraded independently.

2. Stateless

Every request must contain all information needed to understand it. The server holds no client context between requests. Session state lives entirely on the client (in a token or cookie).

This is what makes REST APIs scalable — any server instance can handle any request without shared session memory.

3. Cacheable

Responses must declare whether they are cacheable. Clients (and proxies) can cache responses to reduce server load. A response marked Cache-Control: max-age=3600 can be served from cache for an hour without touching the server.

4. Uniform Interface

All resources use the same interface:

  • Resource identification — each resource has a unique URL (/users/42, not /getUser?id=42)
  • Manipulation through representations — client sends a representation (JSON body) to modify the resource
  • Self-descriptive messages — each response includes enough information to understand it (Content-Type, status code)
  • HATEOAS — responses include links to related actions (often skipped in practice)

5. Layered System

The client cannot tell if it's talking directly to the server or through a load balancer, cache, or API gateway. Each layer only knows about the layer immediately next to it.

6. Code on Demand (Optional)

Servers can extend client functionality by sending executable code (JavaScript). The only optional constraint.


URL Design — Resources, Not Actions

The most visible part of a REST API is its URL structure. URLs should represent nouns (resources), not verbs (actions). The HTTP method supplies the verb.

WRONG (RPC-style, not REST):
GET  /getUser?id=42
POST /createUser
POST /deleteUser?id=42
POST /updateUserRole

CORRECT (REST):
GET    /users/42           → fetch user 42
POST   /users              → create a new user
DELETE /users/42           → delete user 42
PATCH  /users/42           → partially update user 42
GET    /users/42/orders    → get orders belonging to user 42
POST   /users/42/orders    → create an order for user 42

Rules for good REST URLs:

  • Use nouns, not verbs
  • Use plural resource names (/users, not /user)
  • Nest to show relationships (/users/42/orders/7)
  • Keep it lowercase with hyphens (/blog-posts, not /blogPosts)
  • Never include actions in the URL (/users/42/delete is wrong)

Status Codes in API Responses

Return the semantically correct status code — not just 200 for everything.

// Creating a resource — return 201, not 200
app.post("/users", async (req, res) => {
  const user = await db.createUser(req.body);
  return res.status(201).json(user);   // 201 Created
});

// Successful delete with no body — return 204, not 200
app.delete("/users/:id", async (req, res) => {
  await db.deleteUser(req.params.id);
  return res.status(204).send();       // 204 No Content
});

// Resource not found — return 404, not 200 with an error body
app.get("/users/:id", async (req, res) => {
  const user = await db.findUser(req.params.id);
  if (!user) {
    return res.status(404).json({ error: "User not found" }); // 404 Not Found
  }
  return res.status(200).json(user);
});

// Validation error — return 400, not 500
app.post("/users", async (req, res) => {
  if (!req.body.email) {
    return res.status(400).json({ error: "email is required" }); // 400 Bad Request
  }
  // ...
});

Critical Request Headers

These are the headers that matter most day-to-day when building or consuming APIs:

Content-Type and Accept

# Client sending JSON → must declare it
Content-Type: application/json

# Client expecting JSON back
Accept: application/json

# Server responding with JSON
Content-Type: application/json; charset=utf-8

# Client uploading a file
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary...

A common bug: sending JSON without Content-Type: application/json. The server's body parser reads it as raw text and returns a 400 error.

Authorization Header — Two Formats

# Bearer token (JWT or OAuth access token)
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

# Basic auth — base64("username:password")
Authorization: Basic cmFraWJ1bDpteXBhc3N3b3Jk

# API key (varies by provider — some use custom header)
Authorization: ApiKey sk-1234567890abcdef
X-API-Key: sk-1234567890abcdef   ← alternative custom header

Bearer vs Basic:

  • Basic: credentials sent on every request (base64 encoded, not encrypted — must use HTTPS)
  • Bearer: short-lived token acquired once via login; server validates the token, not the password
// Node.js: reading Authorization header
app.get("/protected", (req, res) => {
  const auth = req.headers["authorization"];     // "Bearer eyJ..."
  if (!auth || !auth.startsWith("Bearer ")) {
    return res.status(401).json({ error: "No token provided" });
  }
  const token = auth.split(" ")[1];              // "eyJ..."
  const decoded = verifyJWT(token);              // validate signature
  if (!decoded) {
    return res.status(401).json({ error: "Invalid token" });
  }
  req.user = decoded;
  next();
});

Cookies vs Tokens — Same Problem, Different Approach

Both cookies and tokens solve the same problem: maintaining identity on a stateless HTTP protocol.

1. POST /login {email, password}

2. Server validates credentials
   Creates session record in DB: { session_id: "abc123", user_id: 42, expires: ... }
   Sends: Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Strict

3. Browser stores cookie automatically

4. GET /dashboard
   Cookie: session_id=abc123   ← browser sends automatically

5. Server looks up session_id → finds user_id 42 → returns dashboard

Pros: Easy revocation (delete the session row). Cookie is opaque — no data leaks. Cons: Requires server-side session storage (Redis/DB). Harder for non-browser clients.

JWT Token-Based Auth

1. POST /login {email, password}

2. Server validates credentials
   Creates JWT: { header.payload.signature }
   Payload: { user_id: 42, role: "admin", exp: 1734567890 }
   Sends JWT in response body (not Set-Cookie)

3. Client stores JWT in memory or localStorage

4. GET /dashboard
   Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...   ← client sends manually

5. Server verifies signature + checks expiry → reads user_id 42 from payload
   No database lookup needed

Pros: Stateless — any server instance can validate. Works for mobile, SPAs, cross-domain. Cons: Hard to revoke (token valid until expiry). If stolen, attacker has full access until expiry.

// JWT verification — no DB lookup needed
const jwt = require("jsonwebtoken");

function verifyToken(token) {
  try {
    // verifyJWT checks: signature valid? Not expired?
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    // decoded = { user_id: 42, role: "admin", iat: ..., exp: ... }
    return decoded;
  } catch (err) {
    // TokenExpiredError or JsonWebTokenError
    return null;
  }
}

HTTP Caching — Reducing Redundant Requests

Caching lets clients and proxies skip network requests when the response has not changed.

Cache-Control Header

# Server: this response is good for 1 hour
Cache-Control: max-age=3600

# Server: cache but revalidate before use
Cache-Control: no-cache

# Server: never cache (sensitive data — banking, medical)
Cache-Control: no-store

# Server: cache publicly (CDN can cache this)
Cache-Control: public, max-age=86400

# Server: cache privately (only the browser, not CDN)
Cache-Control: private, max-age=3600

no-cache vs no-store:

  • no-cache — can cache but must revalidate with server before using. If server says "still fresh" (304), use the cache. Never serves stale without checking.
  • no-store — do not store at all, ever. Used for responses containing secrets.

ETag — Conditional Requests

An ETag is a fingerprint of the response content (usually a hash). The client stores it and sends it back on the next request. If the content has not changed, the server sends 304 Not Modified with no body — saving bandwidth.

# First request
GET /api/products HTTP/2
→ 200 OK
   ETag: "d41d8cd98f00b204e9800998ecf8427e"
   Cache-Control: no-cache
   [body: large JSON array]

# Second request (client sends stored ETag)
GET /api/products HTTP/2
If-None-Match: "d41d8cd98f00b204e9800998ecf8427e"

→ 304 Not Modified
   ETag: "d41d8cd98f00b204e9800998ecf8427e"
   [no body — client uses cached version]

The ETag approach is powerful for APIs: clients always get fresh data when it changes, but avoid re-downloading large payloads when nothing changed.

// Express: sending ETags
const express = require("express");
const crypto = require("crypto");
const app = express();

app.get("/api/products", async (req, res) => {
  const products = await db.getProducts();
  const body = JSON.stringify(products);

  // Generate ETag from content hash
  const etag = crypto.createHash("md5").update(body).digest("hex");
  res.setHeader("ETag", `"${etag}"`);
  res.setHeader("Cache-Control", "no-cache");

  // Check if client already has this version
  if (req.headers["if-none-match"] === `"${etag}"`) {
    return res.status(304).send();    // Not Modified — no body sent
  }

  return res.status(200).json(products);
});

Last-Modified — Time-Based Validation

An alternative to ETag using timestamps:

# First response
Last-Modified: Tue, 15 Apr 2025 09:00:00 GMT

# Client sends on subsequent request
If-Modified-Since: Tue, 15 Apr 2025 09:00:00 GMT

# Server: content is unchanged
→ 304 Not Modified

HATEOAS (Hypermedia As The Engine Of Application State) is the most often skipped REST constraint. A fully RESTful response includes links to available actions:

{
  "id": 42,
  "name": "Rakibul Hasan",
  "email": "mail.liilab@gmail.com",
  "_links": {
    "self":    { "href": "/users/42",        "method": "GET" },
    "update":  { "href": "/users/42",        "method": "PATCH" },
    "delete":  { "href": "/users/42",        "method": "DELETE" },
    "orders":  { "href": "/users/42/orders", "method": "GET" }
  }
}

In practice, most APIs skip HATEOAS. It is rarely implemented but frequently asked about in interviews to check if you know the formal REST definition.


Common Mistakes

  • Putting actions in URLs instead of using HTTP methods. /users/42/delete is not REST — it is RPC over HTTP. The correct form is DELETE /users/42. URL should identify the resource; the method supplies the action.

  • Returning 200 for all responses including errors. Returning { "error": "User not found" } with a 200 OK status defeats the purpose of HTTP status codes. Proxies, caches, and monitoring tools use status codes. Return 404 for not found, 400 for bad input, 401/403 for auth failures. Only return 200 when the operation genuinely succeeded.

  • Confusing Cache-Control: no-cache with "don't cache." no-cache does not mean "do not cache." It means "cache it, but revalidate before use." To prevent any caching, use no-store. This distinction is especially important for sensitive data like user profiles or financial information where you truly want nothing stored.


Interview Questions

Q: What are the REST constraints? Which ones are most important?

REST has 6 constraints: Client-Server separation, Statelessness, Cacheability, Uniform Interface, Layered System, and Code on Demand (optional). The most commonly tested are Statelessness (every request is self-contained, no server-side session) and Uniform Interface (resources identified by URLs, HTTP methods as verbs, self-descriptive messages). An API that violates Statelessness — for example by storing client session state on the server — is not truly RESTful regardless of how its URLs look.

Q: What is the difference between a cookie and a JWT for authentication?

Both solve stateful auth on a stateless protocol. Cookies carry a session ID that the server uses to look up session state from a database or Redis. JWTs are self-contained tokens — the user's identity and claims are encoded in the token itself, signed by the server. The server validates the signature without a database lookup. Cookies are easier to revoke (delete the session row), more secure against XSS when HttpOnly (JavaScript can't read them), but require server-side storage. JWTs are stateless and work well for distributed systems and cross-domain APIs, but revocation requires a blocklist (or waiting for expiry), and if stored in localStorage they are accessible to JavaScript and vulnerable to XSS.

Q: What does Cache-Control: no-cache mean?

Despite the name, it does not mean "do not cache." It means "you may store this response, but you must revalidate it with the origin server before using the cached copy." The server typically uses an ETag or Last-Modified header for revalidation. If the content has not changed, the server returns 304 Not Modified and the client uses its cached version without re-downloading the body. To truly prevent any caching, use Cache-Control: no-store.

Q: How does ETag-based caching work?

When a server sends a response, it includes an ETag header containing a fingerprint (usually a hash) of the response content. The client stores both the response body and the ETag. On the next request for the same resource, the client sends If-None-Match: <stored-etag>. The server computes the current ETag and compares. If identical (content unchanged), it responds with 304 Not Modified and no body — the client uses its cached copy. If different (content changed), the server responds with 200 OK and the new body plus a new ETag. This eliminates redundant data transfer while ensuring clients always get fresh data when it changes.


Quick Reference — Cheat Sheet

REST URL Design

Resource:    /users                /users/42          /users/42/orders
GET:         list all users        get user 42        list orders for user 42
POST:        create a user         —                  create order for user 42
PUT:         —                     replace user 42    —
PATCH:       —                     update user 42     —
DELETE:      —                     delete user 42     —

Status Code Guide

CodeWhen to Use
200Successful GET, successful PATCH/PUT with body
201Successful POST (resource created)
204Successful DELETE or PATCH with no body
400Invalid request body, missing required fields
401Not authenticated (no token, expired token)
403Authenticated but not authorized
404Resource not found
409Conflict (duplicate email, version mismatch)
422Validation failed (well-formed but semantically wrong)
429Rate limit exceeded
500Unexpected server error

Cache-Control Quick Reference

DirectiveMeaning
max-age=NCache is valid for N seconds from response time
no-cacheCache but revalidate before each use
no-storeNever store — for sensitive data
publicCDN and shared caches can store
privateOnly the user's browser can store (not CDN)
must-revalidateExpired cache must not be used without revalidation

Cookies vs JWT Comparison

AspectCookie (session)JWT
Storage locationServer DB / RedisClient (memory / localStorage)
XSS protectionHttpOnly flagVulnerable if in localStorage
CSRF protectionRequires SameSite/CSRF tokenNot vulnerable (not auto-sent)
RevocationDelete session rowBlocklist or wait for expiry
Cross-domainBlocked by SameSiteWorks with Authorization header
DB lookup per requestYesNo

Previous: Lesson 8.1 → Next: Lesson 8.3 →


This is Lesson 8.2 of the Networking Interview Prep Course — 8 chapters, 32 lessons.

On this page