10LOC
Bunadvanced

Bundling programmatically with Bun.build()

Published December 4, 2026

try {
  const result = await Bun.build({
    entrypoints: ["./src/index.ts"],
    outdir: "./dist",
    minify: true,
  });

  for (const artifact of result.outputs) console.log(artifact.kind, artifact.path);
} catch (err) {
  const error = err as AggregateError;
  console.error(`Build failed with ${error.errors.length} error(s)`, error);
}

export {};

What

Bun.build({ entrypoints, outdir, ... }) runs Bun's native bundler from JavaScript and resolves to { success, outputs, logs } — the same bundler behind the bun build CLI, callable programmatically as part of a larger script.

Why it matters

Shelling out to bun build from a script means parsing CLI flags out of an object, capturing stdout, and re-parsing text to know what happened. Bun.build() skips all of that: options are a typed object, and the result is structured data — an array of BuildArtifact objects (each one a Blob with .path, .kind, and .loader) plus a logs array of warnings. That makes it straightforward to build tooling around bundling — a custom CLI, a deploy script that inspects what got produced, or a dev server that bundles on demand and streams an artifact straight into a Response.

How it works

  • entrypoints and outdir mirror the CLI's positional entry file and --outdir; minify: true is the JS-API equivalent of --minify.
  • On a genuine build failure (a syntax error, an unresolved import), Bun.build() rejects the promise with an AggregateError rather than resolving with success: false — that's why the real error handling here is the catch block, not an if check on the resolved value. error.errors is an array of BuildMessage/ResolveMessage instances with a .message and .position describing exactly where each failure is.
  • On success, result.outputs holds every generated file — entrypoints, chunks, sourcemaps — each already a Blob-like object, so artifact.text() or new Response(artifact) works without a separate Bun.file() read.
  • result.logs (not shown failing here) still gets populated with non-fatal warnings even on a successful build — worth checking in CI even when the build otherwise "passes."

Gotchas

  • Don't reach for result.success as the primary failure signal the way you might expect from the CLI's exit code — an uncaught rejection here crashes the script if it isn't wrapped in try/catch (or a .catch()).
  • outdir writes files with content-hashed or path-based names depending on naming — the bundled entrypoint usually isn't literally outdir/index.js unless the entrypoint file is named that.
  • Bundling every time a script runs has a real cost; for a dev server this is fine (Bun.build() is fast), but a CI pipeline that rebuilds unnecessarily on every step still pays that cost each time.

Related

  • bun build --compile produces a standalone executable; the same behavior is available as Bun.build({ compile: { outfile } }) for scripting a cross-compiled release.
  • The Bun.$ shell scripting post covers wrapping a build step in cleanup/versioning logic alongside Bun.build().