Build Tools & Dev Servers
Browsers don't understand TypeScript, and they don't know how to resolve
import { Todo } from "@app/shared" into an actual file. Somewhere between the code you
write and the code a browser runs, a tool has to bridge that gap — twice over, once quickly for
development and once thoroughly for production.
This chapter's examples already assume a .vue component file, and it's worth pausing
on that before going further. Building a browser UI doesn't require any particular framework —
you could write it in plain JavaScript with no framework at all. In practice, almost nobody does,
because a framework gives you a consistent way to structure UI as reusable pieces, manage the data
those pieces depend on, and keep the screen in sync with that data as it changes. That problem has
several well-established, actively competing answers — Vue,
React,
Angular, and
Svelte among them — and each is
genuinely opinionated about how you should write UI code. None of them is objectively correct; they
trade off differently on syntax, performance characteristics, and how much structure they impose.
This is a Vue-centric course. Every component example from here through Chapter 8 is written in Vue's syntax, not React's or anyone else's. The underlying ideas — components, reactive state, one-way data flow, a build step that bridges dev and production — carry over to any framework you use later; the exact syntax will not. Vue was picked here for reasons that are more practical than doctrinal: it's a mature, well-documented choice with a large ecosystem of tested libraries to draw on, and its component syntax stays close to plain HTML/JS, which keeps the examples readable if you've never used a framework before.
Module resolution and transforms #
Every import statement in your code is a request: "find me this other file." A
bundler
has to resolve each one — is it a relative path, a workspace package, or something installed from
a registry? — and then read and process the file it finds.
"Process" usually means transpiling: stripping TypeScript type annotations, converting component-file syntax into plain JavaScript function calls, and generally turning whatever you wrote into something a browser's JavaScript engine already knows how to run. None of this changes what your code does — it changes what dialect it's written in.
source, before transpiling// greet.ts
export function greet(name: string): string {
return `Hello, ${name}!`;
}
roughly what a browser actually receives
// greet.js
export function greet(name) {
return `Hello, ${name}!`;
}
Looking at those two snippets side by side, it's fair to wonder why you'd bother with the longer
one. The answer is that greet.ts was never written for the browser — it was
written for you, and for the type checker from Chapter
3 reading over your shoulder while you write it. The : string annotation catches a
caller passing a number before your code ever runs; your editor uses that same annotation to
autocomplete name's string methods and to flag greet(42) as an error while
you're still typing it. None of that exists in greet.js — by the time transpiling
strips the types away, they've already done their job. You're not writing TypeScript so the browser
can run it faster or smaller; you're writing it so mistakes surface on your screen, in your editor,
before they ever reach a browser at all.
Two very different modes: dev and build #
Modern tooling (this course uses Vite as its example) runs in two distinct modes that solve different problems.
In build mode, the tool's job is to produce the smallest, fastest set of static files for production: bundling many source files into few output files, removing code that's never actually used (tree-shaking), minifying names and whitespace, and fingerprinting filenames for long-term caching. This takes real time — seconds to minutes — because it's optimizing thoroughly, once, for many future page loads.
In dev mode, the priority flips entirely: the tool optimizes for the time between you saving a file and seeing the result, not for the smallest possible output. A dev server like Vite's serves your source files essentially as-is, transpiling each module on demand as the browser requests it, rather than bundling the entire application up front.
Hot Module Replacement #
Hot Module Replacement (HMR) goes a step further than "reload fast" — it swaps just the changed module in the running page without a full reload at all. If you're editing a component and change a line of its template, HMR can patch that component in place while the rest of the app — its current in-memory state, scroll position, open modal — stays exactly as it was.
This matters more than it sounds. Without HMR, changing one line of a form component means: reload the page, re-navigate to that form, re-fill every field, and re-trigger whatever state got you there. With HMR, the edit appears instantly, mid-interaction. It's the single biggest reason modern frontend development feels fast.
HMR is a dev-mode-only feature. Production builds don't include it — there's no "running state to preserve" in a static file server, and shipping the machinery that makes HMR work would only add dead weight to your production bundle.
Dev-server proxying #
During development you typically run two separate processes: a dev server for the frontend (fast
rebuilds, HMR) and a backend server (handling /api/* routes, talking to a database). They
listen on different ports. Without help, a fetch('/api/todos') call from a page served by
the frontend dev server would try to hit the frontend dev server's own port and get a 404 — there's
no /api/todos route there.
The fix is a proxy rule in the dev server's config: requests matching a given path prefix are forwarded to the other process, transparently, so the browser never needs to know two servers are involved.
dev server config, proxying API calls to a separate backend process// vite.config.ts
import { defineConfig } from "vite";
export default defineConfig({
server: {
proxy: {
"/api": {
target: "http://localhost:4000",
changeOrigin: true,
},
},
},
});
defineConfig doesn't change what the object contains — it exists purely so your
editor can type-check and autocomplete the config, and you'll see it wrapping the config object in
essentially every real Vite project.
With that rule in place, the same fetch('/api/todos') call works identically whether
you're running the dev server locally or hitting a production build where the backend serves the built
frontend directly and there's only one process to begin with. Your application code never needs to know
which situation it's in. changeOrigin rewrites the request's origin header to match the
target, which some backends require to accept the proxied request; it also means dev and production
can behave slightly differently around cookies, which Chapter 9's CORS discussion covers.
Hardcoding a full URL like fetch('http://localhost:4000/api/todos') in frontend code
works in dev but breaks the moment you deploy somewhere the backend isn't on localhost:4000.
Use relative paths (/api/todos) and let the dev-server proxy or production server handle
routing — the frontend code shouldn't need to know where the backend lives.
Open the project you scaffolded in Chapter 4 (or
scaffold one now if you skipped it: pnpm create vite@latest my-project -- --template
vue-ts, then cd my-project && pnpm install) and start its dev server:
pnpm dev
With the server running, open src/App.vue in an editor and change some visible text
— a heading, a button label. Save the file and look at the browser without refreshing it
yourself: the text updates on its own, in place, with no full-page reload. That's HMR, not a fast
page refresh — if you had clicked into an input or scrolled down first, you'd see that state
survive the edit too.
Then open vite.config.ts at the project root. There's no server.proxy
block in it yet — the starter template doesn't need one because it has no separate backend to
talk to. Add one now, pointed at a port nothing is running on (http://localhost:4000 is
fine), following the shape shown earlier in this chapter. It won't do anything visible today; you're
just confirming you can find and edit the exact file this chapter has been describing. Chapter 9
gives you a real backend to point it at.
Why this matters for everything that follows #
Every chapter from here on assumes a dev server sitting between you and the browser: transpiling TypeScript and component files on the fly, hot-swapping your edits, and proxying API calls to a backend process running alongside it. When Chapter 9 introduces that backend process and Chapter 6 introduces the component syntax being transpiled, you'll already understand the tool making both of them usable together during development.