cheat sheet

esbuild

Daily-driver reference for esbuild — bundle CLI, transform API, JS API patterns, common build recipes.

esbuild — CLI & transform API

What it is

esbuild is the Go-implemented bundler/transformer that set the modern speed baseline. It powers Vite's dev transforms, ships embedded inside most modern toolchains, and works standalone as a CLI or JS API. This page covers the CLI and the JS transform API.

For package context, see packages-npm/npm-esbuild.

Install

bash
npm install -D esbuild
# or
npm install -g esbuild

Output: platform-specific Go binary in node_modules/.bin/esbuild.

Day-to-day commands

CommandWhat it does
esbuild src/index.ts --bundle --outfile=dist/out.jsBasic bundle
esbuild src/index.ts --bundle --platform=nodeBundle for Node (CJS resolution)
esbuild src/index.ts --bundle --platform=browserBundle for browsers (default)
esbuild src/index.ts --bundle --minifyMinified output
esbuild src/index.ts --bundle --watchWatch mode (rebuilds on change)
esbuild src/index.ts --bundle --servedir=distTiny dev server (no HMR)
esbuild src/index.ts --bundle --target=es2020,chrome90,safari15Down-level to targets
esbuild src/index.ts --bundle --metafile=meta.jsonEmit build metadata
esbuild src/index.ts --analyzePrint bundle composition summary
esbuild --versionPrint esbuild version

Common scenarios

Bundle a CLI for Node

bash
npx esbuild src/cli.ts \
  --bundle \
  --platform=node \
  --target=node20 \
  --outfile=dist/cli.cjs

Output:

text
  dist/cli.cjs   1.2mb

⚡ Done in 47ms

For a tiny shipping binary, add --minify. For ESM output, --format=esm (and rename to .mjs).

Browser app bundle with code splitting

bash
npx esbuild src/index.ts src/worker.ts \
  --bundle \
  --format=esm \
  --splitting \
  --outdir=dist \
  --minify \
  --target=es2020

Output:

text
  dist/index.js          12kb
  dist/worker.js          8kb
  dist/chunk-AB12.js      4kb   ← shared chunk

⚡ Done in 89ms

--splitting requires --format=esm (and --outdir, not --outfile).

Transform API (no bundling, no FS)

javascript
import { transform } from "esbuild";

const result = await transform(
  "const x: number = 42; export default x;",
  { loader: "ts", target: "es2022" }
);

console.log(result.code);

Output:

text
const x = 42;
export default x;

transform takes a single string in, returns code + map. No filesystem access. Use for REPLs, AST tools, runtime evaluation.

Watch / context API

javascript
import esbuild from "esbuild";

const ctx = await esbuild.context({
  entryPoints: ["src/index.ts"],
  bundle: true,
  outfile: "dist/out.js",
});

await ctx.watch();         // rebuilds on file change
const { host, port } = await ctx.serve({ servedir: "dist" });
console.log(`http://${host}:${port}`);

serve is a minimal HTTP server — no HMR. Use Vite for SPA dev.

Sourcemap modes

bash
npx esbuild src/index.ts --bundle --sourcemap            # external (.map file + comment)
npx esbuild src/index.ts --bundle --sourcemap=inline     # base64 in JS file
npx esbuild src/index.ts --bundle --sourcemap=external   # .map file, no comment
npx esbuild src/index.ts --bundle --sourcemap=linked     # .map file + comment (default)

Output:

text
  out.js      12.3kb
  out.js.map  18.7kb

⚡ Done in 17ms

For prod: --sourcemap=external + upload to error tracker, or --sourcemap=linked + serve maps privately.

Plugin (JS API only)

javascript
import esbuild from "esbuild";

const envPlugin = {
  name: "env",
  setup(build) {
    build.onResolve({ filter: /^env$/ }, (args) => ({
      path: args.path,
      namespace: "env-ns",
    }));
    build.onLoad({ filter: /.*/, namespace: "env-ns" }, () => ({
      contents: JSON.stringify(process.env),
      loader: "json",
    }));
  },
};

await esbuild.build({
  entryPoints: ["src/index.ts"],
  bundle: true,
  outfile: "dist/out.js",
  plugins: [envPlugin],
});

esbuild's plugin API is intentionally small: onResolve, onLoad, onStart, onEnd. Not a webpack-style hook ladder.

Mark deps external

bash
npx esbuild src/index.ts --bundle --platform=node \
  --external:express --external:pg --outfile=dist/out.js

Output:

text
  dist/out.js  8.2kb

⚡ Done in 11ms

Or for all of node_modules/:

bash
npx esbuild src/index.ts --bundle --platform=node \
  --packages=external --outfile=dist/out.js

Output:

text
  dist/out.js  4.7kb

⚡ Done in 9ms

The --packages=external flag (0.18+) externalises every npm package, leaving only your source bundled.

Respect tsconfig.json

bash
npx esbuild src/index.ts --bundle --tsconfig=tsconfig.json --outfile=dist/out.js

Output:

text
  dist/out.js  18.4kb

⚡ Done in 21ms

esbuild reads target, paths, experimentalDecorators, useDefineForClassFields. Without --tsconfig, it falls back to its own defaults.

Useful flags

FlagWhat it does
--bundleResolve imports and bundle into one (or splittable chunks)
--outfile=<path>Single-file output
--outdir=<path>Multi-file output
--format=<fmt>esm, cjs, iife
--platform=<plat>browser (default), node, neutral
--target=<targets>e.g. es2020,chrome110,safari16,node20
--external:<name>Exclude from bundle (resolve at runtime)
--packages=externalAll node_modules/ deps external (0.18+)
--splittingCode splitting (ESM-only, multi-entry)
--minifyWhitespace + identifiers + syntax
--minify-whitespace, --minify-identifiers, --minify-syntaxGranular
--sourcemap[=mode]Sourcemap generation
--watchRebuild on change
--servedir=<dir>Tiny dev server
--tsconfig=<path>Use a specific tsconfig
--loader:<.ext>=<loader>Map extension to loader (file, text, binary, dataurl, base64, css, json)
--define:<key>=<val>String substitution (--define:process.env.NODE_ENV=\"production\")
--banner:js=<str> / --footer:js=<str>Wrap output
--legal-comments=<mode>none, inline, eof, linked, external
--metafile=<path>Emit build metadata JSON
--analyzePrint bundle treemap summary
--log-level=<level>silent, error, warning, info, debug, verbose
--color / --no-colorControl coloured output

Configuration

esbuild has no config file. Either pass flags on the CLI or write a build.mjs:

javascript
// build.mjs
import esbuild from "esbuild";

await esbuild.build({
  entryPoints: ["src/index.ts"],
  bundle: true,
  platform: "browser",
  format: "esm",
  outdir: "dist",
  splitting: true,
  minify: true,
  target: ["es2020", "chrome110", "firefox110"],
  sourcemap: true,
  define: {
    "process.env.NODE_ENV": '"production"',
  },
  loader: { ".png": "file", ".svg": "text" },
  metafile: true,
});
bash
node build.mjs

Output:

text
  dist/index.js     12kb
  dist/chunk-AB.js   4kb

⚡ Done in 89ms

The JS API is more flexible than the CLI — multi-entry, plugin chains, metafile inspection.

Common pitfalls

  1. [ERROR] Could not resolve "X". Either missing install, missing --platform=node for Node deps, or a tsconfig.json path alias not loaded (pass --tsconfig).
  2. Top-level await is not available in the configured target environment. Bump --target=es2022+ or refactor.
  3. The CommonJS "require" function does not work in ESM scope. Output is ESM but source uses require(). Switch source, change --format=cjs, or add a createRequire shim.
  4. Source maps don't show original source. --sources-content=false was set, or IDE loads bundle from a different URL than sourceMappingURL expects.
  5. Bundle includes node_modules/ deps unexpectedly. Default bundles everything. Use --packages=external or per-package --external:X.
  6. Binary fails after install. Platform optional dep missing. Run npm install --include=optional or npm rebuild esbuild.
  7. Minifier produces slightly larger output than Terser. Expected — esbuild trades ~5% bytes for ~10× speed. Run Terser as a second pass for max compression.

See also