CHAPTER 05 · FRONTEND

Web Platform Fundamentals

Backend engineers know HTTP, but usually only from the server's side. This chapter moves to the browser's side: what the browser does for you, which security rules it enforces, and how it turns HTML and CSS into pixels. These rules don't change no matter which framework you use.

⏱ ~50 minDeep (HTTP, cookies, CORS) Use (HTML, Tailwind) Aware (CSS details)
By the end of this chapter you should be able to:
  • Walk through the main steps from typing a URL to an interactive page, in order.
  • Explain why a request that works with curl gets blocked by CORS in the browser, and the right direction for fixing it.
  • Justify putting session credentials in an HttpOnly cookie instead of localStorage.
  • Read Tailwind code and describe UI requirements to an agent in precise terms.

Opening: curl works, the browser doesn't

Opening question

LinkNote's frontend lives at app.linknote.dev and the API at api.linknote.dev. Calling the API with curl works perfectly; the fetch in the page logs blocked by CORS policy. The agent suggests "set Access-Control-Allow-Origin to * on the server". Why do curl and the browser behave differently? What's wrong with the agent's suggestion?

Where the answer lies: the cross-origin restriction isn't a server rule; it's a rule the browser enforces to protect users. curl isn't a browser, so it isn't restricted. The browser blocks because it automatically attaches the user's cookies — if any site could read any other site's responses, a malicious page could read your data as you. Setting * may "make the error go away", but it cannot be combined with cookies and it opens the API to every origin. The right direction: declare exactly which origins are allowed, or avoid cross-origin altogether by putting frontend and API on the same origin.

To really understand this, start from a full page load.

From typing a URL to an interactive page

Network ① DNSdomain → IP ② TCP + TLSencrypted connection ③ HTTP requestGET / + cookies ④ Server / CDNmay hit CDN cache first ⑤ HTML responsestatus + headers + body Browser ⑥ Parse HTML → DOMfetch CSS/JS as found ⑦ CSS → CSSOMCSS blocks rendering ⑧ Layout + paintpositions → pixels (first visible) ⑨ Run JSbind events → interactive Afterwards ⑩ User acts → JS calls fetch → updates DOM No full reload: this is how single-page apps work; React turns data changes into DOM changes (Ch. 06). The rendering mode decides what's in ⑤ Client rendering: ⑤ is an empty shell; content after ⑨. Server rendering: ⑤ has full content, visible at ⑧ (Ch. 07).
The main path of a page load (simplified: HTTP/2 multiplexing, preloading, cache hits etc. omitted).

You'll reuse this diagram: in Chapter 07, "why server rendering shows the first screen faster" is really a discussion of what's inside ⑤ and how much JS ⑨ needs.

HTTP: the parts you need to own Deep

As a backend developer you know methods and status codes. Here are the points that matter most in full-stack work:

StatusMeaningTypical full-stack scenario
200 / 201 / 204OK / Created / No ContentNormal request; bookmark created; delete succeeded
301 / 302 / 307 / 308RedirectsPost-login redirect; 307/308 keep the method
304Not Modified (use cache)ETag revalidation hit
400 / 422Malformed / failed validationReturn on Zod failure with per-field errors
401 / 403Unauthenticated (who are you?) / Forbidden (I know you, but no)Often confused — keep them apart
404 / 409Not found / ConflictSaving the same URL twice may return 409
429Too many requestsRate limiting; you'll see it often from LLM APIs
500 / 502 / 503 / 504Server error / bad gateway / unavailable / gateway timeout504 is common when serverless functions time out

One more thing: HTTP is stateless. The server doesn't "remember" who sent the previous request. "Being logged in" means every request carries a credential, and in browsers the most common carrier is a cookie.

Cookies and login: what the browser does "automatically" Deep

The key property of cookies: the browser automatically attaches them to requests sent to the matching domain, with no JS involved. That's why they're convenient — and the root of a whole class of security problems.

AttributeEffectWithout it
HttpOnlyJS can't read it (document.cookie doesn't see it)If a malicious script gets injected (XSS), the session credential is stolen outright
SecureOnly sent over HTTPSCan be sniffed on insecure networks
SameSite=LaxNot sent on requests initiated by other sites (except top-level navigations)Cross-site request forgery (CSRF): a malicious site makes your browser call your API with your cookie
Max-Age / ExpiresExpiryDies when the browser closes (session cookie)
Where do tokens go: cookie or localStorage?

Many tutorials (and agents) store a JWT in localStorage and add it to request headers manually. The problem: localStorage is readable by any JS on the page, so one XSS steals the token. For your own site, the safer default is putting the session credential in an HttpOnly + Secure + SameSite cookie. Mainstream auth libraries (Better Auth, Auth.js, Clerk) do this by default. localStorage is fine for non-sensitive UI preferences.

Same-origin policy and CORS Deep

An origin = scheme + host + port. https://app.linknote.dev and https://api.linknote.dev are different origins; so are http://localhost:5173 and http://localhost:8787.

The same-origin policy: by default the browser won't let JS from one origin read responses from another. CORS (Cross-Origin Resource Sharing) is how a server "opts out": it declares in response headers which origins may read it.

Browser page origin: app.linknote.dev API server api.linknote.dev ① OPTIONS /links (preflight) Origin: https://app.linknote.dev ② Access-Control-Allow-Origin: https://app.linknote.dev Access-Control-Allow-Credentials: true ③ POST /links (the real request, with cookies) ④ 201 + same CORS headers → only now can JS read it
Non-simple requests (e.g. a POST with a JSON body) send a preflight first. The server's answer decides whether the browser lets it through.

Back to the agent's suggestion in the opening:

The better fix is often to avoid cross-origin entirely: put frontend and API on the same origin. Full-stack frameworks (Next.js, TanStack Start) do this naturally — pages and API routes come from the same service; or split by path under one domain (/api/* goes to the backend). In development, use the dev server's proxy.

Caching: send the same thing only once Use

Browsers, CDNs and servers may all cache responses, mostly controlled by the Cache-Control header. The standard modern frontend strategy is "two kinds of files, two policies":

ResourceFile nameCache policyWhy
JS / CSS / imagesapp.3f9a1c.js (content hash)public, max-age=31536000, immutableContent change → name change, so old files can be cached forever
HTMLindex.htmlno-cache (revalidate every time)HTML references the latest hashed names; it must be fresh
API responses—Depends; user-private data should be private or uncachedSo the CDN never serves user A's data to user B

Bundlers add hashes automatically and platforms set sensible defaults. You need to know the mechanism to debug "deployed, but users still see the old version" and "the CDN cached something it shouldn't have".

The DOM: the browser's UI data structure

The browser parses HTML into an object tree called the DOM (Document Object Model). JS can read and write this tree, and the browser reflects changes on screen.

That's imperative code: you tell the browser how to change things step by step. Fine for simple pages; when the same data shows in five places and you must also handle loading, error and empty states, keeping the DOM in sync by hand spirals out of control. That's exactly what libraries like React solve — you describe "what the UI should look like", and it figures out "how the DOM must change".

Day to day you'll rarely touch the DOM directly, but know that DOM events bubble (click a child and the parent's listener also fires), and forms submit and reload the page by default (hence the ubiquitous e.preventDefault()). Aware

HTML: choosing the right tag matters more than you think Use

A common problem in agent code: a <div onClick> used as a button. It "works", but keyboards can't focus it, screen readers don't know it's a button, and it won't submit a form. Semantic HTML gives you all that for free:

NeedUseDon't use
Click to perform an action<button type="button"><div onClick>
Go to another page<a href> (or the framework's <Link>)<button onClick={navigate}>
Collect input and submit<form> + <label> + <input>Loose inputs + reading values on button click
Page structure<header> <nav> <main> <article><div> everywhere

These details fall under "accessibility" (a11y). You don't need to be an expert, but put "all main actions can be done with the keyboard alone" in your acceptance criteria.

CSS: reading layouts is enough Aware

CSS is a huge field. For a full-stack developer in transition, master just these concepts and leave the rest to agents and component libraries:

Tailwind CSS and shadcn/ui Use

Tailwind is the default for agent-generated UI in 2026. Instead of CSS files, it provides lots of "utility classes", each mapping to one style rule, written directly on elements:

How to read it: p-4 = 1rem padding; mt-1 = top margin; flex flex-wrap gap-2 = flex layout, wrapping, with gaps; prefixes like hover:, dark:, md: mean "only in that state or at that screen size".

Why does it suit the AI era? Style lives with structure, so an agent editing one component doesn't hunt for CSS across files; the class names are a finite, standardized vocabulary models know extremely well; nobody has to invent CSS class names. The cost is long class lists, which matters little when agents write the code.

What changed in Tailwind v4

v4 moved configuration out of tailwind.config.js into your CSS file (@import "tailwindcss"; plus @theme { ... } for design tokens). Models often generate v3-style config. When you see an agent create tailwind.config.js, check which version the project uses.

shadcn/ui isn't a traditional component-library dependency. It's a set of component source files that a CLI copies into your project (pnpm dlx shadcn@latest add button dialog); after that you edit them freely. Components sit on top of unstyled, accessible primitives (e.g. Radix UI) and are styled with Tailwind. It became mainstream for the same reason as Tailwind: the code is in your repo, where the agent can read and change it.

Browser APIs worth knowing Aware

APIPurpose
fetchHTTP requests (also on server runtimes)
URL / URLSearchParamsParse and build URLs and query strings. Don't concatenate strings by hand
FormDataRead form data; React form Actions receive it directly
localStorage / IndexedDBLocal browser storage: simple key-value / large structured data
EventSource (SSE)One-way server push; common for LLM streaming
WebSocketTwo-way realtime: chat, collaboration
Web WorkerRun computation on a background thread to avoid blocking the main one
Tell the agent: vocabulary for describing UI

UI requirements are the easiest to state vaguely. These words make descriptions precise:

Vague
Precise
"Make a nice list page."
"Bookmark list page: single column on mobile, two-column grid at ≥768px, three at ≥1280px. Each card shows title (one line, truncated), summary (max two lines), tags (pill style). Four states: loading uses skeletons, empty state with a call-to-action button, error state with retry, and the normal list. Use shadcn/ui's Card component."
"The API has a CORS problem, fix it."
"The frontend at app.linknote.dev calls api.linknote.dev and must send the session cookie. On the API, allow only that origin, enable credentials, and handle OPTIONS preflight. Don't use the * wildcard. Also evaluate: would switching to a same-origin /api path be simpler?"

Useful vocabulary: responsive breakpoints, skeleton, empty state, loading / error states, optimistic update, debounce, infinite scroll / pagination, modal / dialog, drawer / sheet, toast, focus management, keyboard accessible.

Interview corner
  • What happens between typing a URL and seeing the page? Go through network → server → parsing → rendering → running JS, then go deep on one step you know well (TLS handshake, CDN caching, the critical rendering path).
  • What is CORS, and why is there a preflight? Start with what the same-origin policy protects (users, because browsers attach credentials automatically), then CORS as the server-declared exemption; requests with custom headers or JSON bodies need a preflight first.
  • What are XSS and CSRF, and how do you defend against them? XSS: injected script runs in your page → escape output (React does by default), CSP, HttpOnly cookies. CSRF: another site sends requests with your cookies → SameSite cookies, CSRF tokens, checking Origin.

Self-check

1. In development the frontend runs on localhost:5173 and the API on localhost:8787. Same origin? If not, what's the easiest fix?

Different origins (different ports). The easiest fix is a proxy in the Vite dev server: the browser requests localhost:5173/api/* and the dev server forwards it to 8787. To the browser, that's same-origin. In production, solve it with same-domain deployment or correct CORS config.

2. After a release some users report a "blank page", and the console shows a 404 for app.3f9a1c.js. What might be going on?

The user's browser (or the CDN) cached the old HTML, which references old hashed file names, and the new deployment removed those files. Check whether HTML was wrongly given a long cache lifetime and whether the platform keeps old asset versions around.

3. A cookie has HttpOnly but no SameSite (and the browser applies legacy rules). Does it stop XSS from stealing the token? Does it stop CSRF?

It stops an XSS script from reading the token (though injected script can still send requests as the user from the page). It doesn't stop CSRF: the cookie is still sent automatically on requests initiated by other sites. Modern browsers treat cookies without SameSite as Lax by default, but don't rely on defaults — set it explicitly.