Networking Fundamentals for React Developers
Build a working mental model of how data travels from a React app to a server and back — DNS, TCP/IP, TLS, HTTP, headers, cookies, caching, CORS, and the REST/JSON/WebSocket patterns React apps use daily — so that fetch calls, API bugs, and CORS errors stop being mysterious.
Every fetch() call in a React app sits on top of a deep stack of networking machinery that most day-to-day component code never has to think about — until something goes wrong: a CORS error, a stale cached response, a cookie that isn't being sent, a WebSocket that won't stay connected. This lesson is a broad, organized survey of that stack, from resolving a domain name to parsing a JSON response body, written specifically for the problems React developers actually hit: talking to REST APIs, handling authentication cookies, dealing with browser caching and CDNs, and understanding why the browser enforces CORS at all. It is intentionally wide rather than deep on any single topic — treat it as the map that tells you what to look up in more detail when a specific problem demands it.
From a URL to a connection: DNS, IP, and TCP/IP
When your app calls fetch('https://api.example.com/users'), the browser first needs an IP address for api.example.com. DNS (Domain Name System) is the distributed, hierarchical lookup system that translates human-readable hostnames into numeric IP addresses; the browser (or OS) checks local caches first, then queries DNS resolvers, which in turn may query root, top-level-domain (.com), and authoritative name servers until an IP address is found. This lookup is typically cached for a Time-To-Live (TTL) period so repeat requests to the same host skip the full resolution chain.
An IP address (IPv4, like 93.184.216.34, or IPv6, like the longer hex-colon form) identifies a specific machine (or, more precisely, a network interface) on the internet. Once the browser has an IP, it opens a TCP connection to that address on a specific port (443 for HTTPS, 80 for HTTP by default). TCP (Transmission Control Protocol) is the transport-layer protocol that provides a reliable, ordered, error-checked byte stream between two endpoints — it handles the 'three-way handshake' to establish a connection, breaks data into packets, retransmits lost packets, and reassembles everything in order on the receiving end. This is the 'TCP/IP' most networking discussions refer to: IP handles addressing and routing packets across networks, TCP handles reliable delivery on top of that.
For HTTPS, once the TCP connection is open, a TLS (Transport Layer Security) handshake follows: the client and server negotiate a shared symmetric encryption key, the server presents a certificate (issued by a trusted Certificate Authority) proving its identity, and — once complete — all further bytes on this connection are encrypted and authenticated. Only after DNS resolution, the TCP handshake, and the TLS handshake have all completed does the browser actually send the HTTP request.
HTTP and HTTPS: requests, responses, headers, and body
HTTP (HyperText Transfer Protocol) is the application-layer protocol that defines the format of requests and responses exchanged over the TCP (and, for HTTPS, TLS-encrypted) connection. HTTPS is simply HTTP run over a TLS-encrypted connection — same request/response semantics, but protected against eavesdropping and tampering in transit. Virtually all production APIs, and all fetch calls from a page served over HTTPS, use HTTPS; browsers actively block many features (and increasingly, plain HTTP requests themselves) on non-HTTPS origins.
An HTTP request has four parts: a method (GET, POST, PUT, PATCH, DELETE, etc., indicating the intended action), a URL/path identifying the resource, a set of headers (key-value metadata like Content-Type, Authorization, Accept), and, for methods like POST/PUT/PATCH, an optional body carrying the actual payload (commonly JSON for REST APIs). A response mirrors this shape: a status code (200 OK, 201 Created, 301/302 redirects, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 500 Internal Server Error, etc.), response headers, and an optional response body.
Headers carry critical metadata for both directions: Content-Type tells the receiver how to parse the body (application/json, text/html, multipart/form-data for file uploads); Authorization typically carries a bearer token (Authorization: Bearer <token>) for authenticated API calls; Accept tells the server what response formats the client can handle; Set-Cookie (response) and Cookie (request) manage cookies; Cache-Control, ETag, and Last-Modified govern caching behavior; and CORS-related headers (Access-Control-Allow-Origin and friends) govern cross-origin access, covered below.
• GET — retrieve a resource, no body, safe/idempotent, cacheable by default. • POST — create a resource or trigger an action, body carries the payload, not idempotent or cacheable by default. • PUT — replace a resource entirely, idempotent. • PATCH — partially update a resource, not necessarily idempotent. • DELETE — remove a resource.
Cookies, caching, and CDNs
Cookies are small key-value pairs a server asks the browser to store via a Set-Cookie response header, which the browser then automatically re-attaches to future requests to that (or a matching) domain via the Cookie request header. They are the classic mechanism for session-based authentication: after login, the server sets a session cookie, and every subsequent request automatically carries it, no client-side code required. Important cookie attributes include HttpOnly (inaccessible to JavaScript, mitigating XSS token theft), Secure (only sent over HTTPS), and SameSite (Strict/Lax/None, controlling whether the cookie is sent on cross-site requests — directly relevant to CORS-based APIs that need cookies, which additionally require credentials: 'include' on the fetch call and specific CORS headers on the server).
HTTP caching lets browsers (and intermediate proxies/CDNs) avoid re-fetching unchanged resources. Cache-Control: max-age=3600 tells the browser it may reuse a cached response for up to an hour without even asking the server; ETag and Last-Modified/If-Modified-Since enable conditional requests, where the browser asks 'has this changed since I last saw it?' and the server can reply 304 Not Modified with no body, saving bandwidth while still confirming freshness. Getting caching right matters directly for React apps: a misconfigured cache can make an API return stale data after a mutation, while an under-cached static asset (like a hashed JS bundle) wastes bandwidth needlessly.
A CDN (Content Delivery Network) is a geographically distributed network of servers that cache and serve content (static assets, and sometimes API responses) from a location physically close to the requesting user, reducing latency and offloading traffic from the origin server. Most production React apps serve their built JS/CSS bundles from a CDN; understanding cache headers matters here too, since CDNs respect the same Cache-Control semantics (plus their own edge-specific overrides) to decide how long to keep serving a cached copy before re-checking the origin.
Compression and CORS
Compression reduces the number of bytes actually sent over the wire. The client advertises what it can decode via the Accept-Encoding request header (commonly gzip, deflate, br for Brotli), and if the server supports one of those, it compresses the response body and marks it with a Content-Encoding response header; the browser decompresses it transparently before handing the body to JavaScript, so fetch/response.json() never has to think about compression directly. This is largely automatic infrastructure-level behavior but is worth knowing about when reasoning about response size versus what actually crossed the network.
CORS (Cross-Origin Resource Sharing) is a browser-enforced security mechanism that restricts whether JavaScript running on one origin (scheme + host + port) can read responses from a different origin. By default, the Same-Origin Policy blocks this; CORS is the opt-in mechanism a server uses to relax it, via response headers like Access-Control-Allow-Origin: https://myapp.com (or * for any origin, which is incompatible with credentialed requests), Access-Control-Allow-Methods, Access-Control-Allow-Headers, and Access-Control-Allow-Credentials. For 'simple' requests the browser sends the actual request directly and just checks the response headers; for anything more complex (custom headers, methods like PUT/DELETE/PATCH, Content-Type: application/json on some setups), the browser first sends an OPTIONS 'preflight' request to ask permission before sending the real one. This is entirely a browser-side enforcement — the request often does reach the server either way, but the browser withholds the response from your JavaScript if the CORS headers don't permit it, which is exactly why CORS errors are invisible in tools that don't run inside a browser context (like curl or Postman) but very visible in a React app's console.
• Same-origin: identical scheme + host + port — no CORS restrictions apply. • Cross-origin: any difference in scheme, host, or port — subject to CORS, enforced by the browser, not the server or network. • Simple request: browser sends it directly, then checks Access-Control-Allow-Origin on the response. • Preflighted request: browser first sends an OPTIONS request; only proceeds with the real request if the server approves it. • Credentialed request (cookies): requires fetch's credentials: 'include' plus Access-Control-Allow-Credentials: true and a specific (non-wildcard) Access-Control-Allow-Origin.
REST, JSON, and WebSocket in practice
REST (Representational State Transfer) is an architectural style for HTTP APIs, not a protocol: it organizes an API around resources identified by URLs (/users, /users/42), manipulated via standard HTTP methods (GET to read, POST to create, PUT/PATCH to update, DELETE to remove), aiming to be stateless (each request carries everything needed to understand it, with the server not relying on server-side session state between requests beyond what's identified in the request itself, such as a token). Most React apps talk to REST (or REST-adjacent) APIs almost exclusively through fetch or a wrapper library, exchanging JSON payloads.
JSON (JavaScript Object Notation) is the near-universal data interchange format for modern web APIs: a text-based, language-independent format that maps directly onto JavaScript object/array/string/number/boolean/null literals, which is exactly why it pairs so naturally with a JavaScript-based frontend — JSON.stringify turns a JS value into a JSON string for a request body, and response.json() parses a JSON response body back into a JS value, with no manual parsing required.
WebSocket is a different protocol from HTTP request/response, used when a React app needs persistent, bidirectional, low-latency communication — live chat, collaborative editing, real-time notifications, live price feeds. A WebSocket connection starts as an HTTP request with an Upgrade: websocket header; if the server agrees, the same underlying TCP connection is 'upgraded' from HTTP semantics to the WebSocket protocol, after which either side can send messages to the other at any time, without the strict request-then-response pattern HTTP requires. This is fundamentally different from polling a REST endpoint repeatedly (which is still request/response, just automated) — a WebSocket keeps one connection open and pushes data as it happens.
Code Examples
fetchCurrentUser sends both a bearer token header and cookies (credentials: 'include'), which is common when a session cookie handles login state while a token handles API authorization; because the token is sent as a custom Authorization header and the method/credentials combination isn't 'simple', the browser will send a CORS preflight (OPTIONS) before the real GET on a cross-origin request, and the server must respond with matching Access-Control-Allow-* headers, including Access-Control-Allow-Credentials: true, for the browser to allow the response through. updateUserName shows a PATCH with a JSON body, requiring both Content-Type: application/json and a JSON.stringify'd body — mismatching Content-Type against the actual body shape is a very common source of confusing 400 errors from REST APIs.
credentials: "include" Common mistake: Forgetting credentials: 'include' on a cross-origin request that needs to send a session cookie — the browser will simply not attach the cookie, and the server will treat the request as unauthenticated, often producing a confusing 401 rather than an obvious CORS error.
getPrice illustrates a typical REST/JSON call: one HTTP request/response cycle, with the browser transparently applying HTTP caching rules based on whatever headers the previous response carried. usePriceStream illustrates the alternative for live-updating data: a single WebSocket connection (note the wss:// scheme — WebSocket over TLS, the WS equivalent of HTTPS) stays open, and the server pushes new price messages over it as they occur, with no repeated request/response round trips; the effect's cleanup function closes the socket when the component unmounts or symbol changes, which matters because WebSocket connections are not automatically cleaned up the way a one-shot fetch is. Expected result: getPrice resolves once per call; usePriceStream's onUpdate fires repeatedly as the server pushes new prices
async function fetchCurrentUser() {
const response = await fetch("https://api.example.com/me", {
method: "GET",
credentials: "include", // send cookies cross-origin (requires server CORS support)
headers: {
"Accept": "application/json",
"Authorization": `Bearer ${getAccessToken()}`,
},
});
if (!response.ok) {
// response.ok is false for any status outside 200-299
if (response.status === 401) {
throw new Error("Not authenticated");
}
throw new Error(`Request failed: ${response.status}`);
}
const contentType = response.headers.get("Content-Type") ?? "";
if (!contentType.includes("application/json")) {
throw new Error("Expected a JSON response");
}
return response.json(); // parses the body as JSON
}
async function updateUserName(name: string) {
const response = await fetch("https://api.example.com/me", {
method: "PATCH",
credentials: "include",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${getAccessToken()}`,
},
body: JSON.stringify({ name }),
});
if (!response.ok) throw new Error(`Update failed: ${response.status}`);
return response.json();
}// REST: one request per fetch, cache handled by HTTP headers automatically
async function getPrice(symbol: string): Promise<number> {
const res = await fetch(`https://api.example.com/prices/${symbol}`, {
headers: { "Accept": "application/json" },
// browser will honor Cache-Control/ETag from a previous response
// automatically unless we override with cache: "no-store", etc.
});
const data = await res.json();
return data.price;
}
// WebSocket: one persistent connection, server pushes updates as they happen
function usePriceStream(symbol: string, onUpdate: (price: number) => void) {
useEffect(() => {
const socket = new WebSocket(`wss://api.example.com/stream/${symbol}`);
socket.onmessage = (event) => {
const { price } = JSON.parse(event.data);
onUpdate(price);
};
socket.onerror = (err) => console.error("WebSocket error", err);
return () => socket.close(); // clean up the connection on unmount
}, [symbol, onUpdate]);
}From fetch() to a JSON response
Each numbered stage corresponds to a concept covered in this lesson: DNS/TCP/TLS establish the connection, the HTTP request/response carries headers/cookies/body, CORS is checked by the browser for cross-origin calls, and compression/caching headers shape what actually crosses the wire before the JSON body reaches your component's state.
React component
│ fetch("https://api.example.com/users", { headers, credentials })
▼
1. DNS lookup: api.example.com → IP address (cached after first lookup)
▼
2. TCP handshake: reliable connection opened to IP:443
▼
3. TLS handshake: certificate verified, encryption keys negotiated (HTTPS only)
▼
4. HTTP request sent over the encrypted connection:
GET /users HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Cookie: session=abc123 (if credentials: "include")
Accept-Encoding: gzip, br
▼
5. [Cross-origin only] Browser checks CORS:
- simple request → send, then check Access-Control-Allow-Origin on response
- non-simple request → OPTIONS preflight first, then the real request
▼
6. Server responds:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Encoding: gzip
Cache-Control: max-age=60
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax
Access-Control-Allow-Origin: https://myapp.com
[gzip-compressed JSON body]
▼
7. Browser decompresses body, decrypts (TLS already h
…Important Rules
The connection sequence for an HTTPS request is: DNS lookup → TCP handshake → TLS handshake → HTTP request/response — each stage must complete before the next begins.
Headers carry metadata (Content-Type, Authorization, Cookie, Cache-Control, CORS headers); the body carries the actual payload, and only certain methods (POST/PUT/PATCH) typically send one.
Cookies set via Set-Cookie are sent automatically by the browser on matching future requests; cross-origin requests only include them when the client explicitly opts in (fetch's credentials: 'include') and the server explicitly allows it via CORS headers.
CORS is enforced by the browser, not the network or the server's own logic — the server can receive a disallowed request just fine, but the browser withholds the response from your JavaScript unless the CORS headers permit it.
A non-'simple' cross-origin request (custom headers, PUT/PATCH/DELETE, certain Content-Types) triggers an automatic OPTIONS preflight before the browser sends the real request.
REST organizes an API around resource URLs and HTTP methods; JSON is the near-universal payload format; WebSocket is a separate, persistent, bidirectional protocol for real-time data, distinct from repeated REST polling.
Why API mutations often invalidate cached GET data
A common real-world bug in React apps is a list that doesn't update after a successful create/update/delete mutation, even though the mutation's own response looked correct. This is frequently a caching issue: the GET request for the list was served with a Cache-Control header that allows the browser (or a data-fetching library's own cache) to reuse the previous response for some time, so re-fetching the list immediately after the mutation returns stale, cached data rather than hitting the server again. Data-fetching libraries built for React (React Query, SWR, RTK Query) exist largely to manage this interaction correctly — they track cache keys per resource and explicitly invalidate or refetch related queries after a mutation succeeds, layered on top of (and coordinated with) whatever HTTP-level caching headers the server also sends.
Assuming a CORS error means the server never received the request.
Treat a CORS error as the browser blocking your JavaScript from reading a response it already received (or in some cases blocking the request before it's sent, for preflighted requests) — check the server's Access-Control-Allow-* response headers, not just server-side logs.
For simple requests, the server often does process the request and send a response; the browser just refuses to expose that response to your code because the CORS headers don't authorize your origin. Confirming this distinction (e.g., via server logs or a non-browser client like curl) saves time debugging in the wrong place.
Sending fetch(url, { credentials: 'include' }) to an API configured with Access-Control-Allow-Origin: '*'.
Configure the server to return a specific origin (Access-Control-Allow-Origin: https://myapp.com) and Access-Control-Allow-Credentials: true when credentials are involved.
The CORS spec disallows the wildcard '*' origin in combination with credentialed requests for security reasons; browsers will block the response even if the server sends both headers together, so credentialed cross-origin APIs must echo back a specific allowed origin.
What is the correct order of operations before an HTTPS request's bytes are actually sent? A. TLS handshake → DNS lookup → TCP handshake → HTTP request B. DNS lookup → TCP handshake → TLS handshake → HTTP request C. TCP handshake → HTTP request → DNS lookup → TLS handshake D. HTTP request → DNS lookup → TCP handshake → TLS handshake
B. DNS lookup → TCP handshake → TLS handshake → HTTP request — The hostname must first be resolved to an IP address (DNS), then a reliable connection opened to that IP (TCP handshake), then encryption negotiated (TLS handshake) before the encrypted HTTP request itself is sent.
Why does the browser enforce CORS, and what is actually being blocked when a CORS error occurs? A. CORS prevents the server from ever receiving the request B. CORS is a browser-enforced restriction on whether client-side JavaScript may read a cross-origin response; the request may still reach the server, but the browser can withhold the response from your code C. CORS is a network-layer firewall rule unrelated to the browser D. CORS only applies to WebSocket connections, not HTTP
B. CORS is a browser-enforced restriction on whether client-side JavaScript may read a cross-origin response; the request may still reach the server, but the browser can withhold the response from your code — CORS is enforced by the browser to protect users from malicious cross-origin JavaScript reading responses it shouldn't. For simple requests, the request itself often reaches the server; the browser just refuses to expose the response to your JS unless permitted by Access-Control-Allow-* headers.
What must a cross-origin server return for a credentialed request (fetch with credentials: 'include') to succeed? A. Access-Control-Allow-Origin: '*' is sufficient on its own B. No special headers are needed if the request uses HTTPS C. A specific (non-wildcard) Access-Control-Allow-Origin matching the requesting origin, plus Access-Control-Allow-Credentials: true D. Only a Set-Cookie header is required
C. A specific (non-wildcard) Access-Control-Allow-Origin matching the requesting origin, plus Access-Control-Allow-Credentials: true — Credentialed cross-origin requests cannot use the wildcard '*' origin; the server must echo back the specific requesting origin and explicitly set Access-Control-Allow-Credentials: true for the browser to allow the credentialed response through.
How does a WebSocket connection fundamentally differ from a typical REST/JSON fetch call? A. WebSocket cannot carry JSON data, only binary B. WebSocket starts as an HTTP request that upgrades the same TCP connection into a persistent, bidirectional channel, rather than following HTTP's one request-then-one response pattern C. WebSocket does not use TCP at all D. WebSocket is only usable for file downloads, not live data
B. WebSocket starts as an HTTP request that upgrades the same TCP connection into a persistent, bidirectional channel, rather than following HTTP's one request-then-one response pattern — A WebSocket connection begins as an HTTP request with an Upgrade header; once the server agrees, the same TCP connection switches to the WebSocket protocol, allowing either side to send messages at any time without the strict request/response cycle HTTP requires — ideal for real-time, bidirectional data.
What is the practical difference between Content-Type on a request versus Cache-Control on a response? A. They are two names for the same header B. Content-Type tells the receiver how to parse the body being sent; Cache-Control tells the client/intermediaries how long a response may be reused without re-fetching C. Content-Type controls caching; Cache-Control controls body parsing D. Both only apply to WebSocket connections
B. Content-Type tells the receiver how to parse the body being sent; Cache-Control tells the client/intermediaries how long a response may be reused without re-fetching — Content-Type describes the format of the payload being sent (e.g., application/json) so the receiver knows how to parse it. Cache-Control is a caching directive telling the browser or intermediate caches (like a CDN) how long a response may be reused before revalidating with the server.
A React app hosted at https://app.example.com calls fetch('https://api.example.com/orders', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(order) }) and gets a network-tab entry showing an OPTIONS request followed by a failed POST, with a console error mentioning CORS. Walk through, stage by stage, why the OPTIONS request happened and what the server needs to return for the POST to succeed.
app.example.com and api.example.com differ in host, so this is a cross-origin request, subject to CORS. Because it uses Content-Type: application/json (not one of the 'simple' content types) and includes credentials, the browser first sends an automatic OPTIONS preflight to ask permission. For the preflight to succeed, the server must respond to OPTIONS with Access-Control-Allow-Origin: https://app.example.com (a specific origin, not '*', since credentials are used), Access-Control-Allow-Methods including POST, Access-Control-Allow-Headers including Content-Type, and Access-Control-Allow-Credentials: true. Only once the preflight passes will the browser send the actual POST, and the POST's own response must also include Access-Control-Allow-Origin and Access-Control-Allow-Credentials for the browser to expose its result to the React code.
Consider: is this a same-origin or cross-origin request? Is it a 'simple' request given the Content-Type and credentials used? What headers does a credentialed cross-origin request require on the response?