Networking Fundamentals
The full HTTP picture for frontend engineers: request/response anatomy, methods, status codes, headers, DNS/TLS, REST, caching, and the CORS you will hit on day one — beginner to advanced in one page.
What actually happens on a request. When your app calls an API, the browser (1) resolves the domain to an IP via DNS, (2) opens a TCP connection and negotiates TLS if it's HTTPS, (3) sends an HTTP request (a method, a path, headers, and an optional body), and (4) receives an HTTP response (a status code, headers, and a body). Understanding this pipeline explains most 'why is my request slow / blocked / failing' questions you'll face.
HTTP methods describe intent. GET reads (no body, safe, cacheable), POST creates (has a body, not idempotent — two POSTs make two rows), PUT replaces a whole resource (idempotent), PATCH updates some fields (partial), DELETE removes. Idempotency matters for retries: retrying a GET/PUT/DELETE is safe; retrying a POST can duplicate. This maps one-to-one to your Axios calls and your REST contract.
Status codes tell you what happened. Families: 1xx informational, 2xx success (200 OK, 201 Created, 204 No Content), 3xx redirection (301/302, 304 Not Modified), 4xx client error (400 bad request, 401 unauthenticated, 403 forbidden, 404 not found, 409 conflict, 422 validation, 429 rate-limited), 5xx server error (500, 502 bad gateway, 503 unavailable, 504 timeout). The frontend treats them differently — 401 refresh/redirect to login, 403 show 'not allowed', 429 back off, 5xx retry or show an error UI.
Headers carry the metadata. Requests send Content-Type (what the body is), Accept (what you want back), Authorization (credentials), and Cookie. Responses send Content-Type, Set-Cookie, Cache-Control, ETag, and the Access-Control-Allow-* CORS headers. Most 'it works in Postman but not the browser' bugs are a header/CORS difference — the browser enforces rules a raw HTTP tool does not.
Request/response bodies and content types. JSON (application/json) is the default for APIs. File uploads use multipart/form-data via FormData. Old-style forms use application/x-www-form-urlencoded. The Content-Type you send must match what the server expects, and it also affects CORS (a JSON content type makes a request 'non-simple' and triggers a preflight — see below).
REST in one paragraph. REST models your API as resources (nouns) addressed by URLs, manipulated with the HTTP methods (verbs): GET /expenses (list), GET /expenses/42 (one), POST /expenses (create), PUT/PATCH /expenses/42 (update), DELETE /expenses/42 (remove). Statelessness means each request carries everything needed (e.g. the token) — no server-side session assumed. Good REST uses the right status codes and plural nouns, not verbs in the URL.
DNS and TLS run before every fresh connection. DNS resolves the domain to an IP (cached, but the first lookup adds latency); TLS encrypts the channel and is why you always use HTTPS. Browsers block mixed content — an HTTPS page cannot load HTTP resources — so 'it works locally but breaks in prod' is often a mixed-content or certificate issue. HTTP/2 and HTTP/3 multiplex many requests over one connection, reducing the cost of many small calls.
HTTP caching — the header contract. Cache-Control (e.g. max-age=3600, no-cache, no-store, immutable) tells the browser how long a response is fresh. Conditional requests use validators: the server sends an ETag (or Last-Modified); on the next request the browser sends If-None-Match, and the server replies 304 Not Modified with no body if nothing changed — saving bandwidth. Static assets get long max-age + content hashing; API responses usually use short or no caching. This is separate from your app-level cache (React Query/SWR).
CORS — your day-one pain point. The browser's same-origin policy blocks JavaScript from reading responses from a different origin (scheme + host + port must all match). Your React dev server on localhost:5173 calling your API on localhost:8080 are different origins, so the browser blocks the response — even though the request was sent and the server answered. The error is in the console, never in your catch's data.
How CORS actually works. For 'simple' requests the browser adds an Origin header and checks the response's Access-Control-Allow-Origin. For 'non-simple' requests (methods like PUT/DELETE, custom headers like Authorization, or a JSON Content-Type) the browser first sends an OPTIONS preflight asking which origins/methods/headers are allowed; only if the server answers with matching Access-Control-Allow-* headers does the real request go out. To send cookies cross-origin you need Access-Control-Allow-Credentials: true and a specific (non-*) origin, plus withCredentials on the client.
Fixing CORS the right way. CORS is enforced by the browser but configured on the server — it must return the right Access-Control-Allow-* headers. In development you can also point a dev proxy (Vite's server.proxy, Next.js rewrites) at the API so requests look same-origin. You never 'disable CORS' in the browser for production, and browser flags/extensions that do so only mask the problem locally.
The mental model (memorise this). A request is a method + path + headers + body; a response is a status + headers + body. DNS→TLS→request→response is the pipeline. Status codes drive UX, headers carry the contract, caching is a header negotiation with ETags/304, and CORS is the browser refusing to let JS read a cross-origin response until the server opts in — a server-side fix, never a frontend one.
You already write the other end of this contract. Mapping exceptions to HTTP responses in a Spring `@ControllerAdvice` (or a Vert.x failure handler) is exactly what the frontend reads when it branches on status codes. REST resources and verbs are your `@GetMapping`/`@PostMapping` routes. CORS is purely browser enforcement, so the fix lives where you'd configure a `CorsFilter`/`WebMvcConfigurer` in Spring or a `CorsHandler` in Vert.x — the client can't set `Access-Control-Allow-Origin` on itself. HTTP caching with ETag/If-None-Match/304 is the same conditional-GET you implement server-side with `ResponseEntity` ETags. Idempotency of PUT/DELETE vs POST is the identical concern you weigh when deciding whether a retry can double-charge.
- A request = method + path + headers + body; a response = status + headers + body. DNS → TLS → request → response is the pipeline behind every call.
- Methods carry intent and idempotency: GET/PUT/DELETE are safe to retry; POST is not (it can duplicate). This drives your retry policy.
- Status codes drive UX: 401 → login/refresh, 403 → forbidden message, 404/422 → specific message, 429 → backoff, 5xx → retry/error UI.
- Most 'works in Postman, fails in the browser' bugs are headers or CORS — the browser enforces rules a raw HTTP tool does not.
- You WILL hit CORS immediately: dev server (:5173) and API (:8080) are different origins, so the browser blocks reading the response.
- Non-simple requests (PUT/DELETE, Authorization header, JSON Content-Type) trigger an OPTIONS preflight; the server must answer with Access-Control-Allow-* headers.
- Sending cookies cross-origin needs Access-Control-Allow-Credentials: true, a specific (non-*) origin, and withCredentials on the client.
- CORS is fixed on the SERVER (or a dev proxy), never by disabling it in the browser for production.
- HTTP caching is a header negotiation: Cache-Control for freshness, ETag/If-None-Match → 304 Not Modified to skip re-downloading unchanged bodies.
- HTTPS is non-negotiable — browsers block mixed content, so an HTTPS page can't load HTTP resources.
Worked Code
# --- REQUEST: method + path + headers + optional body ---
POST /api/expenses HTTP/1.1
Host: api.example.com
Content-Type: application/json # what the body IS
Accept: application/json # what we want BACK
Authorization: Bearer eyJhbGci... # credentials
{ "title": "Lunch", "amount": 12.5 }
# --- RESPONSE: status + headers + body ---
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/expenses/42 # where the new resource lives
Cache-Control: no-store
ETag: "a1b2c3"
{ "id": 42, "title": "Lunch", "amount": 12.5 }// Central place to turn a status code into a UX decision
function handleStatus(status: number) {
if (status === 401) window.location.href = '/login'; // not authenticated
else if (status === 403) showToast('You are not allowed to do that');
else if (status === 404) showToast('Not found');
else if (status === 422) showFieldErrors(); // validation
else if (status === 429) scheduleBackoffRetry(); // rate limited
else if (status >= 500) showToast('Server error — try again'); // transient
}
// REST verbs map straight to Axios calls
api.get('/expenses'); // list
api.get('/expenses/42'); // read one
api.post('/expenses', body); // create -> 201 Created
api.put('/expenses/42', body); // full update (idempotent)
api.patch('/expenses/42', body); // partial update
api.delete('/expenses/42'); // remove -> 204 No Content# Browser sends a PREFLIGHT before a non-simple request (PUT + Authorization)
OPTIONS /api/expenses/42 HTTP/1.1
Origin: http://localhost:5173
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization, content-type
# Server MUST answer with matching allow-* headers or the browser blocks it
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:5173 # not * when using credentials
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: authorization, content-type
Access-Control-Allow-Credentials: true # required to send cookies
Access-Control-Max-Age: 600 # cache the preflight// vite.config.ts — proxy /api to the backend so it is same-origin in dev
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8080', // your Spring/Vert.x API
changeOrigin: true,
// rewrite: p => p.replace(/^\/api/, ''), // if the backend has no /api prefix
},
},
},
};
// Now the app fetches '/api/expenses' (same origin as :5173) and the browser
// never sees a cross-origin request — no CORS headers needed in development.# First response is cacheable and carries a validator
HTTP/1.1 200 OK
Cache-Control: max-age=60
ETag: "v1-abc"
{ ...data... }
# Next time the browser asks 'only send it if it changed'
GET /api/config HTTP/1.1
If-None-Match: "v1-abc"
# Nothing changed -> tiny response, no body re-downloaded
HTTP/1.1 304 Not Modified
ETag: "v1-abc"▶Try It Live
Edit the code and press Run — it executes safely in a sandboxed iframe. Use the Console tab for log output.
Interview-Ready Q&A
CORS (Cross-Origin Resource Sharing) is the browser's same-origin-policy enforcement: it blocks JavaScript from reading a response from a different origin (different scheme, host, or port) unless the server explicitly allows it via Access-Control-Allow-Origin. The request may still reach the server and get answered — the browser just refuses to hand the response to your code. The fix is on the server (return the right Access-Control-Allow-* headers, and handle the OPTIONS preflight for non-simple requests). In development you can also use a dev proxy so requests look same-origin. You cannot fix it from frontend code alone.
- 1Pipeline: DNS → TLS → request (method+path+headers+body) → response (status+headers+body).
- 2Methods & idempotency: GET/PUT/DELETE safe to retry; POST is not (can duplicate).
- 3Status families: 2xx success, 3xx redirect/304, 4xx client (401 login, 403 forbidden, 404, 422 validation, 429 backoff), 5xx server.
- 4CORS is browser-enforced, server-fixed: return Access-Control-Allow-* or use a dev proxy — never disable it in prod.
- 5Non-simple requests (PUT/DELETE, Authorization, JSON) trigger an OPTIONS preflight.
- 6Cross-origin cookies need Access-Control-Allow-Credentials: true + a specific origin + withCredentials.
- 7Caching: Cache-Control for freshness; ETag + If-None-Match → 304 to skip re-downloading unchanged bodies.
- 8HTTPS is mandatory — browsers block mixed content; 'works in Postman' usually means a CORS/cookie difference.