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: BearerandAuthorization: Basic- Why
ETagcan eliminate 90% of bandwidth for repeated requests- What
Cache-Control: no-storedoes thatno-cachedoesn'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
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, and304 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/deleteis 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.
Cookie-Based Sessions
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 Links in Responses
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/deleteis not REST — it is RPC over HTTP. The correct form isDELETE /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 a200 OKstatus defeats the purpose of HTTP status codes. Proxies, caches, and monitoring tools use status codes. Return404for not found,400for bad input,401/403for auth failures. Only return200when the operation genuinely succeeded. -
Confusing
Cache-Control: no-cachewith "don't cache."no-cachedoes not mean "do not cache." It means "cache it, but revalidate before use." To prevent any caching, useno-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
ETagheader 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 sendsIf-None-Match: <stored-etag>. The server computes the current ETag and compares. If identical (content unchanged), it responds with304 Not Modifiedand no body — the client uses its cached copy. If different (content changed), the server responds with200 OKand 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
| Code | When to Use |
|---|---|
| 200 | Successful GET, successful PATCH/PUT with body |
| 201 | Successful POST (resource created) |
| 204 | Successful DELETE or PATCH with no body |
| 400 | Invalid request body, missing required fields |
| 401 | Not authenticated (no token, expired token) |
| 403 | Authenticated but not authorized |
| 404 | Resource not found |
| 409 | Conflict (duplicate email, version mismatch) |
| 422 | Validation failed (well-formed but semantically wrong) |
| 429 | Rate limit exceeded |
| 500 | Unexpected server error |
Cache-Control Quick Reference
| Directive | Meaning |
|---|---|
max-age=N | Cache is valid for N seconds from response time |
no-cache | Cache but revalidate before each use |
no-store | Never store — for sensitive data |
public | CDN and shared caches can store |
private | Only the user's browser can store (not CDN) |
must-revalidate | Expired cache must not be used without revalidation |
Cookies vs JWT Comparison
| Aspect | Cookie (session) | JWT |
|---|---|---|
| Storage location | Server DB / Redis | Client (memory / localStorage) |
| XSS protection | HttpOnly flag | Vulnerable if in localStorage |
| CSRF protection | Requires SameSite/CSRF token | Not vulnerable (not auto-sent) |
| Revocation | Delete session row | Blocklist or wait for expiry |
| Cross-domain | Blocked by SameSite | Works with Authorization header |
| DB lookup per request | Yes | No |
Previous: Lesson 8.1 → Next: Lesson 8.3 →
This is Lesson 8.2 of the Networking Interview Prep Course — 8 chapters, 32 lessons.