Test, build, and ship

Bundle server code

Create a production Bun bundle, choose the Bun target, keep source maps, and understand what bundling does not verify.

Bun runs our TypeScript source directly, so we never needed a build step so far. Bundling is optional for a Bun server. But it has a real benefit at deploy time: instead of shipping index.ts, server.ts, database.ts, and all of node_modules, you ship one JavaScript file.

A bundler starts from an entry file, follows every import, and combines everything it finds into one output. Bun has one built in.

Create a Bun-targeted bundle:

bun build \
  --target=bun \
  --outdir=dist \
  --sourcemap=external \
  index.ts

Bun writes dist/index.js and dist/index.js.map, and prints a short summary with the output sizes. Run the result with:

bun dist/index.js

You should see the same Listening on http://localhost:3000/ message. Same server, one file.

Why the target matters

--target=bun tells the bundler the output will run on Bun. It leaves Bun.serve() alone, keeps the bun:sqlite import as a runtime import, and doesn’t try to polyfill Node.js modules.

The default target is browser, which is the wrong choice for server code. Build with it by mistake and the bundler may fail to resolve bun:sqlite, or produce output that only breaks when you start it. If a fresh bundle errors out on an import that worked fine in development, check the target first.

The source map is the second flag worth understanding. Bundled code doesn’t look like your code anymore. Without a map, a stack trace points at line 4,812 of dist/index.js. With --sourcemap=external, Bun writes a separate .map file, and errors point back to database.ts:31 in the original TypeScript. Keep the map with the deployment when your logging system can use it.

Know what the bundle contains

The bundler follows imports from index.ts and combines application code and package code. Zod ends up inside dist/index.js. Files your code opens at runtime do not, unless you explicitly embed them.

Our notes.sqlite database stays outside the bundle. It’s changing application data, not source code, and the bundled server opens it by path the same way the source did.

The bundler transpiles TypeScript, and like bun index.ts it does not check the types. A bundle can build with type errors in it. Run the full verification sequence:

bun run typecheck
bun test
bun build --target=bun --outdir=dist index.ts

Then start the bundle and request /health:

curl http://localhost:3000/health

A successful build only proves Bun produced output. A {"ok":true} from the bundled server proves the output runs. Those are different facts, and I check both before calling a build done.

Lesson completed