The protocol every API you design will speak, the security layer under it, and the caching semantics that most engineers never learn.
Prerequisites: Networking 101, TCP, UDP, IP Time to read: ~22 minutes
TCP gives you a stream of bytes between two machines. That’s not enough to build anything. You need agreed answers to:
HTTP answers the first four. TLS answers the last.
HTTP is text (in 1.1 — binary in 2 and 3, but the semantics are identical). A request:
GET /users/42?fields=name,email HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGci...
Accept: application/json
If-None-Match: "a3f5c9"
User-Agent: MyApp/1.2
And a response:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 47
Cache-Control: private, max-age=60
ETag: "a3f5c9"
{"id":42,"name":"Bilal","email":"b@example.com"}
Four parts to each: a start line, headers, a blank line, and an optional body. That’s the entire protocol structure. Everything else is convention layered on top.
HTTP is stateless. The server remembers nothing between requests. Every request must carry everything needed to serve it (usually a token or cookie). This is not a limitation — it is the property that makes horizontal scaling possible. Any server can serve any request, so you can add and remove servers freely. → Scalability
| Method | Purpose | Safe? | Idempotent? | Cacheable? |
|---|---|---|---|---|
GET |
Read | ✅ | ✅ | ✅ |
HEAD |
Read headers only | ✅ | ✅ | ✅ |
POST |
Create / process | ❌ | ❌ | Rarely |
PUT |
Replace at a known URI | ❌ | ✅ | ❌ |
PATCH |
Partial update | ❌ | ❌ (usually) | ❌ |
DELETE |
Remove | ❌ | ✅ | ❌ |
OPTIONS |
Capability discovery / CORS preflight | ✅ | ✅ | ❌ |
Two words carry most of the weight here:
Safe = no side effects. A crawler, a browser prefetcher, or a corporate proxy may issue safe
requests without asking you. 🚨 This is why GET /deleteUser?id=42 is a genuine catastrophe waiting
for a search engine crawler to find it.
Idempotent = doing it twice has the same effect as doing it once. This is the property that makes retries safe, and retries are unavoidable in distributed systems, because when a request times out you have no idea whether it succeeded.
PUT /users/42 {name: "Bilal"} twice → same final state. Safe to retry.POST /orders twice → two orders. Not safe to retry — and this is how people get double-charged.The fix for POST is an idempotency key: the client generates a unique key, sends it in a header, and the server records it and returns the original result on a repeat. Stripe’s API is the canonical example. → Idempotency ⭐
Learn the classes first: 1xx informational, 2xx success, 3xx redirect, 4xx your fault, 5xx my fault.
| Code | Meaning | When you’d return it |
|---|---|---|
200 OK |
Success with a body | Normal reads |
201 Created |
Resource created | POST that made something; include a Location header |
202 Accepted |
Queued, not done yet | Async work — you’ll poll or get a webhook |
204 No Content |
Success, nothing to say | DELETE, or a PUT with no useful body |
301 / 308 |
Moved permanently | Domain change. Cached aggressively and hard to undo — be careful. |
302 / 307 |
Moved temporarily | Short URL redirects, A/B tests |
304 Not Modified |
Your cached copy is still good | Response to If-None-Match — saves the whole body |
400 Bad Request |
Malformed | Validation failure |
401 Unauthorized |
Not authenticated | Missing/invalid token (badly named — it means unauthenticated) |
403 Forbidden |
Authenticated but not allowed | Wrong permissions |
404 Not Found |
No such resource | Also used to hide existence from unauthorized users |
409 Conflict |
State conflict | Optimistic-concurrency version mismatch, duplicate creation |
422 Unprocessable |
Syntactically fine, semantically wrong | Business-rule violation |
429 Too Many Requests |
Rate limited | Always include Retry-After → Rate Limiting |
500 Internal Server Error |
We broke | Unhandled exception |
502 Bad Gateway |
Upstream returned garbage | Your LB couldn’t get a valid response |
503 Service Unavailable |
Overloaded/maintenance | Shed load here; include Retry-After |
504 Gateway Timeout |
Upstream too slow | The timeout you set fired |
🚨 The 4xx/5xx distinction is not pedantry — it drives behaviour. Clients retry 5xx and 429. They must not retry 400 or 422, because the request will never succeed. Returning 500 for validation errors causes clients to retry forever and turns a bug into an outage. Your monitoring and error budgets also key off this split.
Caching — the most under-used part of HTTP:
| Header | Does |
|---|---|
Cache-Control: public, max-age=3600 |
Anyone may cache for an hour |
Cache-Control: private, max-age=60 |
Only the browser, not shared caches/CDNs |
Cache-Control: no-store |
Never store (use for sensitive data) |
Cache-Control: no-cache |
Store, but revalidate before each use (not “don’t cache”) |
Cache-Control: stale-while-revalidate=60 |
Serve stale instantly, refresh in background — excellent for latency |
ETag: "a3f5c9" |
A version fingerprint for the resource |
If-None-Match: "a3f5c9" |
Client: “only send it if it changed” → gets 304 if not |
Last-Modified / If-Modified-Since |
Same idea, timestamp-based, weaker |
Vary: Accept-Encoding, Authorization |
Cache key must include these headers |
📐 A 304 Not Modified is ~200 bytes instead of a 500 KB body. On a mobile network that’s the
difference between instant and a visible pause. Conditional requests are free performance, and most
APIs never implement them.
🚨 Vary is a classic bug source: forget Vary: Authorization on a CDN-cached endpoint and you
will serve one user’s private data to everyone. This has happened to real companies.
Content negotiation: Content-Type, Accept, Accept-Encoding: gzip, br, Content-Encoding.
Identity: Authorization: Bearer <token>, Cookie / Set-Cookie.
Operational (add these, thank yourself later): X-Request-ID / traceparent for
tracing, Retry-After, and rate-limit headers
(X-RateLimit-Remaining, X-RateLimit-Reset).
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax; Max-Age=86400
The flags are the entire security story:
HttpOnly — JavaScript cannot read it. Blocks token theft via XSS. Use it always for session
cookies.Secure — HTTPS only. Always.SameSite=Lax|Strict — don’t send on cross-site requests. This is the modern CSRF defence.Domain / Path — scope. Setting Domain=.example.com shares the cookie with all
subdomains, which is convenient and occasionally a security hole.HTTP over the open internet is plaintext. Anyone on the path — the coffee shop router, the ISP, a compromised switch — can read every byte and modify it. TLS fixes three separate things:
example.com, not an impostor.That third one is the part people forget, and it’s the hardest. Encryption is easy; knowing who you encrypted to is the whole problem.
sequenceDiagram
participant C as Client
participant S as Server
C->>S: ClientHello (TLS versions, cipher suites, random, SNI)
S->>C: ServerHello (chosen cipher) + Certificate + key share
Note over C: Verify certificate chain against trusted CAs<br/>Check hostname, expiry, revocation
C->>S: key share, Finished
Note over C,S: Both derive the same session key
C->>S: Encrypted HTTP request
TLS 1.3 does this in one round trip (down from two in TLS 1.2), and zero for a resumed session. It also removed all the old broken ciphers. If a design question involves TLS overhead, saying “with TLS 1.3 and session resumption the handshake cost is largely amortized” is a good signal.
A certificate binds a public key to a domain name, signed by a Certificate Authority your OS/browser already trusts. The chain goes: your cert → intermediate CA → root CA (pre-installed).
Practical things to know:
Option A: Client ══TLS══► Load Balancer ──plaintext──► App servers
Option B: Client ══TLS══► Load Balancer ══TLS══► App servers (re-encrypt)
Option C: Client ══════════TLS all the way═══════════► App server (passthrough)
| Option | Pro | Con |
|---|---|---|
| A. Terminate at LB | Cheap; LB can inspect and route on content; centralized cert management | Internal traffic is plaintext — unacceptable in zero-trust or regulated environments |
| B. Re-encrypt | Encrypted end to end; LB still sees content | Double the CPU; certs to manage internally |
| C. Passthrough | LB never sees plaintext | No L7 routing, no content-based rules, per-app cert management |
Most systems do A inside a trusted VPC, and B or a service mesh when compliance or zero-trust demands it. Being able to explain this choice is a solid senior signal.
HSTS (Strict-Transport-Security) tells browsers “always use HTTPS for this domain,” which
closes the window where a user’s first plaintext request could be hijacked.
| Decision | Gain | Cost |
|---|---|---|
Aggressive Cache-Control |
Fewer origin requests, faster users | Stale content; invalidation is hard |
| ETags + conditional requests | Big bandwidth savings | Server must compute/store the ETag |
| TLS everywhere | Security, and required by browsers for modern APIs | CPU + handshake latency (small with 1.3) |
| Terminate TLS at the edge | Fast handshakes near the user, warm origin connections | Internal traffic unencrypted unless re-encrypted |
| Idempotency keys on POST | Safe retries, no double charges | Server must store keys and dedupe |
Idempotency-Key header on POST requests and stores the result for 24
hours, returning the original response on a retry. This is the reference implementation of safe
payments over an unreliable network.304 responses against your rate limit —
a direct incentive to implement conditional requests.GET for mutations, or POST for everything.200 with an error in the body. Clients, proxies, and monitoring all key off status
codes. {"success": false} with a 200 breaks every one of them.Retry-After on 429/503. Without it, clients retry immediately and make the
overload worse.Cache-Control:
public, max-age=3600, stale-while-revalidate=600 and let the CDN absorb almost all of it.”Retry-After rather than 503, so clients back off in a coordinated way
instead of retrying immediately.”# See everything, including the TLS handshake
curl -v https://api.github.com/users/torvalds
# Conditional requests: grab the ETag, then use it
ETAG=$(curl -sI https://api.github.com/users/torvalds | grep -i '^etag:' | cut -d' ' -f2 | tr -d '\r')
curl -s -o /dev/null -w '%{http_code}\n' -H "If-None-Match: $ETAG" https://api.github.com/users/torvalds
# → 304, with no body transferred
# Inspect a certificate chain
openssl s_client -connect example.com:443 -servername example.com </dev/null | head -40
# Check when a cert expires
echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -dates
Then: build a tiny endpoint that supports ETags and conditional GETs. It’s ~10 lines and it will make caching feel real.
POST /payments. Should it retry? What should the API have provided?Cache-Control: no-cache actually mean?