Lesson 1 · Frontend for backend developers
Why there is a build step at all — and what the browser actually receives.
You already know how to make a server return HTML. Flask does it, Django does it, FastAPI plus Jinja does it. So the first honest question about the frontend stack is not "how do I use React" — it is why is any of this necessary.
Start at the top and work down.
A web page is a tree of objects living in the browser's memory. That tree is the DOM. It is not your HTML file; it is what the browser built after parsing your HTML file. JavaScript can reach into that tree and change it.
So far, so simple. The difficulty is this: your application state and the DOM are two separate copies of the same truth, and they drift apart.
Here is that drift, in plain browser JavaScript:
the old way
let count = 0;
button.addEventListener('click', () => {
count += 1; // truth #1: the variable
label.textContent = `Clicked ${count} times`; // truth #2: the DOM
});
Two lines, two copies. Now add a second place that shows the count. And a reset button. And a rule that hides the label when the count is zero. Every one of those features adds another line whose only job is to hand-carry a change from truth #1 to truth #2 — and every one of them is a line you can forget to write.
This is cache invalidation. You have a source of truth and a derived representation, and you are manually keeping the derived one in step. You know from backend work how that ends.
React's answer is to delete truth #2 from your job description. You write a function that returns what the page should look like for a given state, and React works out which DOM operations get the real page there:
the React way
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>;
}
You never write textContent. You change the state; the DOM is a
consequence. That is the entire pitch, and every other piece of the stack is
support scaffolding for it.
In the React version, what updates the text on screen?
Look again at what you just wrote. <button onClick=...>
inside a JavaScript function is not JavaScript. No browser on earth will parse
it. It is JSX, and it has to be translated into real function
calls before a browser sees it:
// what you write
<button onClick={handler}>Hello</button>
// what actually runs
jsx('button', { onClick: handler, children: 'Hello' })
Add TypeScript — which the browser also cannot run, since types are not a JavaScript feature — and you now have two languages that must be compiled before delivery. That is why a build step exists. Not fashion. Necessity.
Vite is the tool that does that translation. It wears two hats, and confusing them causes a lot of pain later, so separate them now:
| Hat | Command | What it does |
|---|---|---|
| Dev server | npm run dev |
Serves your files, translating each one on demand as the browser asks for it. Edit a file and the change is pushed into the running page without a reload (HMR). Optimised for speed of feedback. |
| Bundler | npm run build |
Compiles and packs everything into a small set of optimised static
files in dist/. Optimised for what users download. In Vite 8
the bundling is done by Rolldown. |
npm run dev is uvicorn --reload: a development
convenience you would never point real traffic at. npm run build
produces the artefact you actually ship — and what it produces is just a
folder of static files. Any web server can serve it.
Three commands. You need Node 20.19+ or 22.12+ — check with
node -v.
Scaffold the project. The -- is not a typo; it passes the
flags through npm to the create script.
npm create vite@latest my-app -- --template react-ts
Install the dependencies listed in package.json.
cd my-app
npm install
Start the dev server, then open the URL it prints.
npm run dev
Now the useful part. Open src/App.tsx, change some text, and
save. The browser updates without reloading — that is HMR. Then run
npm run build and look inside dist/. You will find
plain .html, .js and .css. No TypeScript.
No JSX. Confirm with your own eyes that the browser never receives what
you wrote. That single observation is the lesson.
Without scrolling up: name the two jobs Vite does, and say which one produces the thing users download.
Dev server (npm run dev) — on-demand translation plus HMR,
for you. Bundler (npm run build) — optimised static files in
dist/, for users.
You write .tsx — TypeScript + JSX, neither of which a browser runs
↓ Vite
Browser gets .js — plain JavaScript
↓ React
DOM updated for you, from state, instead of by hand
package.json or vite.config.ts that looked
arbitrary — those files are where most of the unexplained magic hides, and
"why is that line there" is always a fair question.