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.
- 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
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
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:
| Status | Meaning | Typical full-stack scenario |
|---|---|---|
200 / 201 / 204 | OK / Created / No Content | Normal request; bookmark created; delete succeeded |
301 / 302 / 307 / 308 | Redirects | Post-login redirect; 307/308 keep the method |
304 | Not Modified (use cache) | ETag revalidation hit |
400 / 422 | Malformed / failed validation | Return on Zod failure with per-field errors |
401 / 403 | Unauthenticated (who are you?) / Forbidden (I know you, but no) | Often confused — keep them apart |
404 / 409 | Not found / Conflict | Saving the same URL twice may return 409 |
429 | Too many requests | Rate limiting; you'll see it often from LLM APIs |
500 / 502 / 503 / 504 | Server error / bad gateway / unavailable / gateway timeout | 504 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.
| Attribute | Effect | Without it |
|---|---|---|
HttpOnly | JS can't read it (document.cookie doesn't see it) | If a malicious script gets injected (XSS), the session credential is stolen outright |
Secure | Only sent over HTTPS | Can be sniffed on insecure networks |
SameSite=Lax | Not 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 / Expires | Expiry | Dies when the browser closes (session cookie) |
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.
Back to the agent's suggestion in the opening:
Access-Control-Allow-Origin: *cannot be combined withAllow-Credentials: true. When cookies are needed, the browser simply refuses — the problem isn't actually solved.- Even without cookies,
*means any website can call your API and read the result. Fine for public data, not for user data.
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":
| Resource | File name | Cache policy | Why |
|---|---|---|---|
| JS / CSS / images | app.3f9a1c.js (content hash) | public, max-age=31536000, immutable | Content change → name change, so old files can be cached forever |
| HTML | index.html | no-cache (revalidate every time) | HTML references the latest hashed names; it must be fresh |
| API responses | — | Depends; user-private data should be private or uncached | So 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:
| Need | Use | Don'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:
- Box model: every element is a box of content, padding, border and margin.
- Flexbox: one-dimensional layout (a row or a column), alignment and distributing leftover space. Nav bars, button groups, card internals.
- Grid: two-dimensional layout. Card grids, overall page layout.
- Responsive design: switch layouts by screen width using media queries. Usually "mobile first": write narrow-screen styles first, then add rules for wider screens.
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.
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
| API | Purpose |
|---|---|
fetch | HTTP requests (also on server runtimes) |
URL / URLSearchParams | Parse and build URLs and query strings. Don't concatenate strings by hand |
FormData | Read form data; React form Actions receive it directly |
localStorage / IndexedDB | Local browser storage: simple key-value / large structured data |
EventSource (SSE) | One-way server push; common for LLM streaming |
WebSocket | Two-way realtime: chat, collaboration |
| Web Worker | Run computation on a background thread to avoid blocking the main one |
UI requirements are the easiest to state vaguely. These words make descriptions precise:
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.
- 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.