Frontend Dev – Build Tooling and Deployment

August 23, 20266 min readUpdated 8/20/2026

The code you write is not the code a browser runs. Between them sits a build step, and after it sits the question of how those files reach a person on the other side of the world. Both are usually somebody else's problem right up until the day they are yours — and the failure modes are specific enough that knowing them is worth an hour.

What a build tool actually does

Browsers cannot run TypeScript or JSX, do not want a thousand separate module requests, and have no idea what import styles from './x.scss' means. The build tool resolves all of that:

JobWhat it means
TransformTypeScript and JSX become JavaScript. Types are erased — the browser never sees one.
BundleFollow the import graph and produce a few files instead of hundreds.
SplitCut the graph at each dynamic import() so rarely-used code is a separate chunk.
Tree-shakeDrop exports nothing imports. Only works because ES modules are statically analysable.
MinifyShorten names, strip whitespace and comments.
HashPut a content hash in each filename, which is what makes caching safe.
Process assetsCompile Sass, inline small images, copy the rest.

Configuration for a modern tool is close to nothing:

export default defineConfig({
  plugins: [react()],
})

That is the demo app's entire Vite config. If you have met older Webpack configurations running to hundreds of lines, this is the thing that changed.

The dev server is not the build

These are two different code paths, which is why "works locally, broken in production" is such a common sentence. The dev server serves modules to the browser more or less as-is for instant startup and hot reloading; the production build does the whole bundle-minify-hash pipeline. Optimisations, environment variables and dead-code elimination can all behave differently.

Always run the production build locally before you shipnpm run build && npm run preview. It takes thirty seconds and catches the class of bug that otherwise gets found by users.

Environment variables are not secret

The most expensive misunderstanding in frontend deployment, and it leaks API keys constantly.

A frontend environment variable is substituted into your bundle at build time. It is then a string in a public JavaScript file that anybody can read. It is configuration, not a secret.

const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8085';

That is a fine use: it changes per environment and is not sensitive — the URL is visible in the network tab regardless. Build tools deliberately require a prefix (VITE_, NEXT_PUBLIC_) so that exposing a variable is an explicit act rather than an accident.

Fine in a frontend variableNever
API base URLDatabase credentials
Publishable/public keys (e.g. Stripe's pk_)Secret keys (sk_) — this is the classic leak
Feature flagsThird-party API tokens
Environment name, build versionAnything you would not print on a billboard

The rule: if it must stay secret, it lives on a server and the browser asks that server. There is no frontend-only way to keep a secret. The corollary is that a build is per-environment — the same artifact cannot be promoted from staging to production if the API URL was baked in, unless you fetch configuration at runtime instead.

Deploying a static frontend

A built SPA is a folder of static files. There is no server to run, which makes hosting cheap and fast: the files go in object storage, a CDN puts copies near your users, and that is the whole architecture. This site is deployed that way and costs well under a dollar a month.

Cache headers, and the two rules that matter

Content hashing is what makes aggressive caching safe. Because index-a3f9c2.js changes its name whenever its contents change, it can be cached forever — a new deploy produces a new name rather than a stale file.

FileCache-ControlWhy
Hashed assets — JS, CSS, imagesmax-age=31536000, immutableThe name changes when the content does.
index.htmlno-cacheIt names the hashed files. Cache it and users get last week's app.

Getting these backwards is the classic deploy bug: everything looks fine for you and users see the old version until they hard-refresh. And if you use a CDN, remember it holds its own copy — invalidate it on deploy, or the new files sit in the bucket unread.

The SPA fallback, which everybody forgets once

Your router handles /orders in the browser. But a user who pastes that URL, or refreshes on it, sends a request for /orders to the server — and there is no file there. You get a 404 on every deep link while the app works perfectly if you navigate from the home page.

The fix is to configure the host to serve index.html for any path that does not match a file. Every static host has a name for this — rewrite rule, fallback document, try_files — and it is a one-line setting. It is also the single most common frontend deployment bug, so check deep links explicitly after your first deploy anywhere new.

When it is not just static files

ApproachWhat runsGood for
SPA (this app)Nothing — static filesApps behind a login, where SEO is irrelevant
SSG — static generationNothing; HTML built ahead of timeContent sites. Fast and cheap, needs a rebuild to change
SSR — server renderingA Node server per requestSEO plus fresh data. Next.js, Nuxt, SvelteKit

An SPA sends an empty page and fills it in with JavaScript. That is fine for a dashboard and bad for a public product page a crawler should read — which is the actual reason the frameworks in the third row exist.

A pipeline worth having

Automate the sequence so it is identical every time and nobody deploys from a laptop with uncommitted changes.

On every pull request, in this order — cheapest first, so quick failures fail quickly:

  1. Install dependencies from the lockfile, so CI builds what you built.
  2. Lint. Seconds.
  3. Typecheck. The demo app exposes this separately as tsc -b --noEmit, because the dev server does not typecheck — it strips types and carries on. A type error can sit in your working app for a week.
  4. Unit and component tests.
  5. Build. A build failure must never first appear during a deploy.
  6. End-to-end tests against the built output.

Then on merge to the main branch: build, upload, set the cache headers, invalidate the CDN, and verify. That last step deserves emphasis — a deploy script that does not check its own result will happily report success having uploaded nothing.

Things worth adding once

  • A bundle-size check that fails the build if the entry chunk grows past a threshold. Bundles do not blow up in one commit; they creep.
  • A Lighthouse run for performance and accessibility scores (post 10).
  • npm audit for known vulnerabilities (post 9).
  • A preview deploy per pull request, so reviewers click the change instead of imagining it. This is the single biggest quality-of-life improvement on most teams.

After it is live

You cannot fix what you never hear about. Users do not file bug reports; they leave.

  • Error reporting — Sentry or similar, catching unhandled errors and rejections with a stack trace. Upload your source maps or every trace is minified gibberish. Do not serve source maps publicly unless you are happy publishing your source.
  • Real user metrics — the numbers from post 10, measured on real devices rather than your laptop.
  • A way back. Deploys break. Being able to roll back in one command is worth more than any amount of pre-release checking. With hashed static files this is usually just re-uploading the previous build.
  • An error boundary so one component's crash shows a message instead of a blank white page.

The backend track's post on deployment and observability covers the same ideas from the server side, where the tooling is richer and the stakes are usually higher.

The one thing to take from this post

Nothing in your frontend build is secret, and nothing about your dev server proves your production build works. Run the real build locally before you ship, get the two cache rules the right way round, configure the SPA fallback before someone pastes a deep link into Slack — and make sure that when it does break, you hear about it from a tool rather than from a customer.

That is the end of the track. Back to Get Started for the map, or across to Backend Development for the other half of the same app.