Lesson 1 · Frontend for backend developers

The stack, from the top

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.

The problem the whole stack exists to solve

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.

In your language

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?

Descending one layer: that code cannot run

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.

Descending again: Vite is the thing that translates

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:

HatCommandWhat 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.
In your language

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.

The floor: do it

Three commands. You need Node 20.19+ or 22.12+ — check with node -v.

  1. 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
  2. Install the dependencies listed in package.json.

    cd my-app
    npm install
  3. 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.

The map you now have

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
Ask Martin. Especially about anything in 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.