● David R. Longnecker
Chapter 5 · Foundations

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.

Wait — why Vue?

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}!`;
}
If the JavaScript is shorter, why not just write that?

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.

Dev mode serves and transforms files on demand per request; build mode bundles, tree-shakes, and minifies everything up front into static output Dev mode Browser requests a module Transform just that file, on demand ms Build mode Bundle + tree-shake + minify every file, up front, once optimizes for feedback speed optimizes for output size/speed Both start from the same source files
Dev mode and build mode both start from your source, but optimize for opposite priorities: iteration speed versus output efficiency.

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.

Key idea

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.

Common mistake

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.

Try it

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.