esbuild is an extremely fast bundler and minifier for JavaScript, TypeScript, JSX, and CSS. It reads your source files, resolves their imports, transforms modern syntax down to the target you choose, and writes optimized output — often 10–100× faster than other bundlers because the whole engine is written in Go and runs as compiled native code with heavy parallelism.
You can use esbuild four ways: as a command-line tool, as a JavaScript/TypeScript library (Node.js), as a browser library (via WebAssembly), and as a Go library. This guide covers everyday usage of all four, with copy-paste-runnable examples for every important option.
Note
This guide documents esbuild v0.28.0. esbuild has not yet reached
1.0.0, so minor versions (0.x) may contain breaking changes. Pin an exact version in production (see Installation).
1. Quick Start
The fastest path from zero to a bundled file.
npm (Node.js)
# Install esbuild as a dev dependency
npm install --save-dev esbuild
# Bundle and minify an app into a single file
npx esbuild app.js --bundle --minify --outfile=out.js
CLI (global)
npm install --global esbuild
esbuild app.js --bundle --minify --outfile=out.js
Go
package main
import (
"os"
"github.com/evanw/esbuild/pkg/api"
)
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Outfile: "out.js",
Bundle: true,
MinifyWhitespace: true,
MinifyIdentifiers: true,
MinifySyntax: true,
Write: true,
LogLevel: api.LogLevelInfo,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
Running the CLI command above prints a one-line build summary:
out.js 1.2kb
⚡ Done in 6ms
That is the whole loop: point esbuild at one or more entry points, tell it to --bundle, and choose where the output goes with --outfile (single file) or --outdir (multiple files).
Tip
Use
npx esbuild(or apackage.jsonscript) instead of a global install. It guarantees every developer and CI machine uses the exact version pinned in yourpackage.json.
2. Installation
Requirements
| Requirement | Detail |
|---|---|
| Node.js | 18 or newer (for the esbuild and esbuild-wasm npm packages) |
| Platform | Native binaries ship for Windows, macOS, Linux, FreeBSD, OpenBSD, NetBSD, Android, illumos/SunOS, AIX, and more, across x64, arm64, arm, ia32, ppc64, s390x, riscv64, loong64, mips64el |
| Go | Only needed if you use the Go library API or build esbuild from source (not needed for the CLI or JS API) |
Important
The
esbuildnpm package contains no binary itself. At install time it pulls a small platform-specific package (e.g.@esbuild/linux-x64,@esbuild/win32-x64,@esbuild/darwin-arm64) through npm'soptionalDependenciesfeature. This is why flags like--no-optionalbreak the install — see Troubleshooting.
Install methods
npm
npm install --save-dev esbuild
pnpm
pnpm add --save-dev esbuild
Yarn
yarn add --dev esbuild
Deno
// Import directly from a URL — no install step
import * as esbuild from "https://deno.land/x/esbuild/mod.js"
Browser (WASM)
npm install --save-dev esbuild-wasm
Verify the installation
npx esbuild --version
0.28.0
Note
The version printed by
--versionmust match the version of theesbuildpackage. esbuild deliberately checks this at startup, because a mismatch almost always means a broken or partially-copied install. If they differ, reinstall.
Pinning a version
Because esbuild is pre-1.0, pin an exact version (no ^ or ~) in production:
{
"devDependencies": {
"esbuild": "0.28.0"
}
}
3. Core Concepts (the 90% you need)
esbuild does two fundamental operations. Everything else is an option on one of them.
| Operation | What it does | CLI | JS API | Go API |
|---|---|---|---|---|
| Build | Reads files from disk, resolves imports, writes output files | esbuild <entry> |
build() / context() |
api.Build() |
| Transform | Transforms a single in-memory string (no file system, no imports) | esbuild < input |
transform() |
api.Transform() |
Use build when you have entry-point files on disk and want bundling, code splitting, or multiple outputs. Use transform when you have a single chunk of source (for example, the contents of one file already in memory) and just want to convert or minify it.
The most common options, in plain terms:
| Option | CLI flag | What it controls |
|---|---|---|
| Bundle | --bundle |
Inline all imported dependencies into the output (off by default) |
| Output file | --outfile=out.js |
Where a single-file build is written |
| Output dir | --outdir=dist |
Where a multi-file build is written |
| Minify | --minify |
Shrink output (whitespace + identifiers + syntax) |
| Source map | --sourcemap |
Emit a .map for debugging original source |
| Target | --target=es2017 |
Lowest JS syntax level / engine to support |
| Format | --format=esm |
Module format of the output (iife, cjs, esm) |
| Platform | --platform=node |
Resolution/defaults for browser, node, or neutral |
| Loader | --loader:.png=dataurl |
How to interpret a given file extension |
Tip
--bundleis opt-in. Without it, esbuild transforms each entry point independently and leavesimport/requirestatements in place. If your output still has bareimport "./foo"lines, you forgot--bundle.
4. Everyday Usage
4.1 Bundling an application
Combine an entry point and everything it imports into one file:
esbuild src/main.ts --bundle --outfile=dist/app.js
dist/app.js 142.7kb
⚡ Done in 11ms
For multiple entry points, use --outdir instead of --outfile:
esbuild src/home.ts src/admin.ts --bundle --outdir=dist
4.2 Minifying
esbuild app.js --minify --outfile=app.min.js
--minify is a shortcut that enables three independent transforms. You can turn them on individually:
| Flag | Effect |
|---|---|
--minify-whitespace |
Removes spaces, newlines, and indentation |
--minify-identifiers |
Renames local variables to short names |
--minify-syntax |
Rewrites code into equivalent shorter forms (e.g. a ? b : c) |
# Minify whitespace only — keeps readable variable names
esbuild app.js --minify-whitespace --outfile=app.js
4.3 Transforming TypeScript or JSX (no bundling)
esbuild strips TypeScript types and converts JSX without a separate compiler. Pipe a file through stdin:
esbuild --loader=ts < input.ts > output.js
Important
esbuild does not type-check. It removes type annotations and emits JavaScript regardless of type errors. Run
tsc --noEmitseparately if you want type checking. This is a deliberate speed trade-off, not a bug.
4.4 Source maps
esbuild app.js --bundle --sourcemap --outfile=out.js
The --sourcemap flag accepts several modes:
| Mode | Flag | Result |
|---|---|---|
| Linked | --sourcemap |
Writes out.js.map and adds a //# sourceMappingURL= comment |
| External | --sourcemap=external |
Writes out.js.map but adds no comment |
| Inline | --sourcemap=inline |
Embeds the map as a base64 data URL inside out.js |
| Both | --sourcemap=both |
Inline map and a separate .map file |
4.5 Choosing a target
Tell esbuild the oldest environment you must support, and it lowers newer syntax accordingly:
# Support these specific engine versions
esbuild app.js --bundle --target=es2017,chrome58,firefox57,safari11,node12 --outfile=out.js
Targets are either an ECMAScript version (es5, es6/es2015, es2016 … es2025, esnext) or an engineVERSION pair (e.g. chrome58, node10, safari11, ie9, edge16, opera45). The default is esnext (assume everything is supported).
Warning
Some modern features (for example, top-level
await) cannot be lowered to very old targets. esbuild will report an error rather than emit incorrect code. Either raise your target or avoid the feature.
4.6 Defining global constants
Replace identifiers at parse time — useful for dead-code elimination of debug branches:
esbuild app.js --bundle --define:DEBUG=false --define:process.env.NODE_ENV=\"production\" --outfile=out.js
Note
Replacement values are raw expressions, not strings. To substitute a string literal you must include the quotes:
--define:VERSION=\"1.0\". To substitute the booleantrue, write--define:RELEASE=true.
4.7 Marking packages as external
Exclude dependencies from the bundle (for example, Node built-ins or packages you ship separately):
# Don't bundle these — leave the import/require in place
esbuild app.js --bundle --platform=node --external:fsevents --external:*.node --outfile=out.js
To keep all node_modules packages external (common for Node libraries):
esbuild app.js --bundle --packages=external --outfile=out.js
4.8 Watch mode
Rebuild automatically when files change. Watch mode is available through the CLI and the JS context() API:
esbuild app.js --bundle --outfile=out.js --watch
[watch] build finished, watching for changes...
Tip
CLI watch mode stops automatically when stdin is closed (so it never outlives its parent process). To keep watching even after stdin closes — for example inside a short-lived VM — use
--watch=forever.
4.9 Local development server
esbuild ships a built-in static server with live rebuilds:
esbuild app.js --bundle --servedir=www --outdir=www/js
> Local: http://127.0.0.1:8000/
| Flag | Purpose |
|---|---|
--serve=8000 |
Listen on a specific port (and optionally host:port) |
--servedir=www |
Serve static files from this directory in addition to build output |
--serve-fallback=index.html |
Serve this page when a request matches no file (SPA routing) |
--certfile= / --keyfile= |
Serve over HTTPS with your TLS certificate and key |
--cors-origin= |
Allow cross-origin requests from the given origin(s) |
Caution
The development server is for local development only. It is not a hardened production web server — do not expose it to the public internet. Serve your built static files with a real web server in production.
5. Common Tasks (recipes)
Bundle a React app for the browser
esbuild src/index.jsx --bundle --minify --sourcemap \
--target=es2018 --format=esm \
--define:process.env.NODE_ENV=\"production\" \
--outfile=public/bundle.js
Bundle a Node.js CLI tool
esbuild src/cli.ts --bundle --platform=node --target=node18 \
--packages=external --outfile=dist/cli.js
Produce both ESM and CommonJS outputs
esbuild src/lib.ts --bundle --format=esm --outfile=dist/lib.mjs
esbuild src/lib.ts --bundle --format=cjs --outfile=dist/lib.cjs
Code splitting (shared chunks between entry points)
esbuild a.js b.js --bundle --splitting --format=esm --outdir=dist
Note
Code splitting (
--splitting) currently requires--format=esm. It extracts code shared between multiple entry points (and dynamicimport()targets) into separate chunk files to avoid duplication.
Inject a shim into every file
# Auto-import a Buffer shim wherever Buffer is used
esbuild app.js --bundle --inject:./buffer-shim.js --outfile=out.js
Bundle CSS
esbuild src/styles.css --bundle --minify --outfile=dist/styles.css
esbuild understands @import rules, url() references, and CSS modules (local-css loader). It can bundle CSS as a first-class entry point, not just CSS imported from JS.
Analyze what's in your bundle
esbuild app.js --bundle --analyze --outfile=out.js
out.js 142.7kb
├ node_modules/react-dom/index.js 130.2kb 91.2%
├ node_modules/react/index.js 6.9kb 4.8%
└ src/app.js 5.6kb 3.9%
Use --analyze=verbose for a deeper per-import breakdown, or --metafile=meta.json to write machine-readable build metadata you can inspect with other tools.
6. The Loader System
A loader tells esbuild how to interpret a file based on its extension. Set loaders with --loader:.ext=name (build) or --loader=name (transform/stdin).
| Loader | Use it for | Behavior |
|---|---|---|
js, jsx, ts, tsx |
Source code | Parse and transform JavaScript/TypeScript/JSX |
json |
.json |
Parse to a JS object (tree-shakeable) |
css |
Global stylesheets | Bundle CSS |
local-css |
CSS Modules | Scope class names locally |
global-css |
CSS | Force global (non-scoped) CSS |
text |
.txt, templates |
Import file contents as a UTF-8 string |
base64 |
Binary | Import as a base64-encoded string |
binary |
Binary | Import as a Uint8Array |
dataurl |
Images, fonts | Inline as a data: URL |
file |
Assets | Copy to output dir, import resolves to the URL path |
copy |
Assets | Copy the file verbatim, import resolves to its path |
empty |
Anything | Replace the file's contents with nothing |
default |
— | Use esbuild's built-in choice for the extension |
Example — treat .png as an inlined data URL and .svg as a copied asset:
esbuild app.js --bundle \
--loader:.png=dataurl \
--loader:.svg=file \
--outfile=out.js
Tip
Use the
fileloader for large assets (they're copied and referenced by URL) anddataurlfor small ones (inlined to avoid an extra HTTP request). The--public-path=flag sets the base URL prepended tofile-loader references.
7. Using esbuild from JavaScript
The JS API gives you everything the CLI does, plus programmatic control. Every option below maps to a CLI flag of the same name (camelCase ⇄ kebab-case).
7.1 build()
import * as esbuild from 'esbuild'
const result = await esbuild.build({
entryPoints: ['src/app.ts'],
bundle: true,
minify: true,
sourcemap: true,
target: ['es2018'],
outfile: 'dist/app.js',
})
console.log(result.warnings) // []
build() returns a BuildResult with errors, warnings, and (depending on options) outputFiles, metafile, and mangleCache. It throws a BuildFailure if the build has errors.
7.2 transform()
Transform a single in-memory string — no file system access:
import { transform } from 'esbuild'
const result = await transform('let x: number = 1', {
loader: 'ts',
minify: true,
})
console.log(result.code) // "let x=1;\n"
7.3 In-memory output (write: false)
Get output as objects instead of writing to disk:
const result = await esbuild.build({
entryPoints: ['app.js'],
bundle: true,
write: false,
})
for (const file of result.outputFiles) {
console.log(file.path, file.text)
}
7.4 Build contexts: rebuild, watch, serve
context() creates a reusable build you can rebuild incrementally, watch, or serve:
const ctx = await esbuild.context({
entryPoints: ['app.js'],
bundle: true,
outdir: 'www/js',
})
await ctx.watch() // rebuild on file changes
const { host, port } = await ctx.serve({ servedir: 'www' })
console.log(`Serving on http://${host}:${port}`)
// Later, when you're done:
await ctx.dispose()
| Method | Purpose |
|---|---|
ctx.rebuild() |
Rebuild now, reusing cached work |
ctx.watch(options?) |
Auto-rebuild on file changes |
ctx.serve(options?) |
Start the dev server |
ctx.cancel() |
Cancel an in-progress rebuild |
ctx.dispose() |
Release the context and stop the background process |
7.5 Synchronous variants
buildSync() and transformSync() block until done. They work only in Node.js (not the browser or Deno) and cannot use plugins.
import { transformSync } from 'esbuild'
const { code } = transformSync('1+2', { minify: true })
Warning
Prefer the async APIs. The synchronous APIs block the event loop and are noticeably slower for repeated calls. They exist for build scripts and tools that cannot use
async/await.
7.6 Plugins
Plugins hook into module resolution and loading. Register them with the plugins option:
const envPlugin = {
name: 'env',
setup(build) {
// Intercept imports of "env"
build.onResolve({ filter: /^env$/ }, args => ({
path: args.path,
namespace: 'env-ns',
}))
// Provide their contents
build.onLoad({ filter: /.*/, namespace: 'env-ns' }, () => ({
contents: JSON.stringify(process.env),
loader: 'json',
}))
},
}
await esbuild.build({
entryPoints: ['app.js'],
bundle: true,
plugins: [envPlugin],
outfile: 'out.js',
})
| Hook | Fires when | Typical use |
|---|---|---|
onResolve |
esbuild needs to resolve an import path | Redirect, externalize, or namespace a path |
onLoad |
esbuild needs a module's contents | Generate or transform source on the fly |
onStart |
Each build (and rebuild) begins | Setup, validation |
onEnd |
Each build finishes | Reporting, post-processing |
onDispose |
The context is disposed | Cleanup |
Important
Plugins require the asynchronous API (
build()orcontext()). They cannot be used withbuildSync()/transformSync().
7.7 Cleaning up (stop())
The JS API keeps a long-lived child process alive for speed. In Node this is cleaned up automatically on exit. In Deno, you must call esbuild.stop() yourself when finished, or the process will keep running.
8. Using esbuild in the Browser
The esbuild-wasm package runs the engine via WebAssembly. You must call initialize() once before any other API call:
import * as esbuild from 'esbuild-wasm'
await esbuild.initialize({
wasmURL: 'https://unpkg.com/esbuild-wasm@0.28.0/esbuild.wasm',
})
const result = await esbuild.transform('let x: number = 1', { loader: 'ts' })
console.log(result.code)
initialize() option |
Meaning |
|---|---|
wasmURL |
URL of the esbuild.wasm file to download |
wasmModule |
A pre-compiled WebAssembly.Module (alternative to wasmURL) |
worker |
Run in a Web Worker (default true) to keep the UI thread free |
Note
In the browser, only
build(),transform(),context(),formatMessages(), andanalyzeMetafile()are available. The*Syncvariants throw — WebAssembly has no synchronous child process.build()works but cannot write to the file system; usewrite: falseand readoutputFiles.
9. Troubleshooting
esbuild's most common problems are installation issues caused by its platform-specific binary packages. The error messages are intentionally detailed — read them fully.
Error: Expected "0.28.0" but got "0.X.Y"
Cause: The installed esbuild JavaScript package and its native binary are different versions, usually from a partial or cached install.
Solution: Reinstall cleanly:
rm -rf node_modules package-lock.json
npm install
Prevention: Pin an exact version and avoid manually editing node_modules.
Error: The package "@esbuild/<platform>" could not be found, and is needed by esbuild
Cause: esbuild was installed with --no-optional or --omit=optional, which skips the platform binary package.
Solution: Reinstall without those flags. esbuild relies on optionalDependencies to fetch the correct binary for your OS/CPU.
Prevention: Never pass --no-optional/--omit=optional when installing esbuild.
Error: You installed esbuild for another platform than the one you're currently using
Cause: A node_modules directory built on one platform was copied to another — classic when copying into a Docker image, or between Windows and WSL.
Solution: Don't copy node_modules across platforms. Copy only your source and package.json, then run npm ci on the destination.
Prevention: Add node_modules to .dockerignore; install dependencies inside the target image.
Error: dyld: Symbol not found: _SecTrustEvaluateWithError (macOS)
Cause: Your version of macOS is too old for the Go compiler that builds the esbuild binary.
Solution: Update macOS, or switch to the esbuild-wasm package, which runs anywhere:
npm uninstall esbuild && npm install esbuild-wasm
Error (macOS, Rosetta 2): architecture mismatch between darwin-x64 and darwin-arm64
Cause: npm ran under Rosetta 2 (installing the x64 binary) while node runs natively as arm64, or vice versa. Solution: Ensure both npm and node run under the same architecture, then reinstall. Installing node from the official universal installer at https://nodejs.org/en/download/ avoids this.
The "esbuild" JavaScript API cannot be bundled
Cause: You bundled the esbuild package itself into another file, breaking the relative path it uses to find its binary.
Solution: Mark esbuild as external when bundling your own tool:
esbuild my-tool.js --bundle --platform=node --external:esbuild --outfile=tool.js
Output still contains import/require statements
Cause: You forgot --bundle. Without it, esbuild transforms files individually and leaves imports intact.
Solution: Add --bundle.
--watch stops on its own
Cause: esbuild ends watch mode when stdin closes, to avoid orphaned processes.
Solution: Keep the parent process alive, or use --watch=forever to ignore stdin.
A modern feature won't compile to my old target
Cause: Some syntax cannot be safely lowered to very old environments.
Solution: Raise --target, or avoid the unsupported feature. Read the error — it names the feature and the conflicting target.
Tip
If npm installs are blocked by a firewall but you have a custom registry, set
NPM_CONFIG_REGISTRY. esbuild's installer (and the Deno loader) honor it when downloading the binary package.
10. Frequently Asked Questions
Q: Does esbuild type-check my TypeScript?
No. esbuild only strips types and transforms syntax — it never reports type errors. Run tsc --noEmit separately for type checking.
Q: Why is the output not bundled?
--bundle is opt-in. Add it. Without it, esbuild processes each entry point independently.
Q: How do I get a single output file?
Use --outfile=out.js. Use --outdir=dir only for multiple entry points or code splitting.
Q: Can I use plugins from the command line?
No. Plugins are a JavaScript/Go API feature. Use the build()/context() API (or the Go pkg/cli RunWithPlugins) to load plugins.
Q: Does esbuild support code splitting?
Yes, with --splitting — currently only when --format=esm.
Q: What's the difference between esbuild and esbuild-wasm?
esbuild runs a native binary (fast). esbuild-wasm runs WebAssembly (portable, works in browsers, but can be several times slower). Use esbuild-wasm only when the native binary won't run.
Q: Can I run esbuild without installing it globally?
Yes — npx esbuild …, a package.json script, or the programmatic API all use your locally pinned version.
Q: How do I substitute an environment variable at build time?
Use --define, remembering that values are raw expressions: --define:process.env.NODE_ENV=\"production\".
Q: Does esbuild minify CSS?
Yes. Pass a .css entry point with --minify (and --bundle to follow @import).
Q: How do I keep node_modules out of my bundle?
Use --packages=external (all packages) or --external:<name> (specific ones).
Q: Why does my Deno script never exit after using esbuild?
Deno can't auto-clean the child process. Call await esbuild.stop() when you're done.
Q: Can esbuild target both ESM and CommonJS?
Yes — run it twice with --format=esm and --format=cjs, choosing different output extensions (e.g. .mjs/.cjs or --out-extension:.js=.mjs).
Q: Is esbuild production-ready even though it's 0.x?
It is widely used in production, but being pre-1.0 means minor releases can introduce breaking changes. Pin an exact version and read the changelog before upgrading.
Q: How do I see what's taking up space in my bundle?
Use --analyze (or --analyze=verbose) for a printed report, or --metafile=meta.json for machine-readable data.
11. CLI Quick Reference
Simple options
| Flag | Description |
|---|---|
--bundle |
Inline all dependencies into the output |
--define:K=V |
Replace identifier K with expression V while parsing |
--external:M |
Exclude module M from the bundle (supports * wildcards) |
--format=iife|cjs|esm |
Output module format |
--loader:.ext=L |
Use loader L for files of extension .ext |
--minify |
Minify output (whitespace + identifiers + syntax) |
--outdir=dir |
Output directory (for multiple entry points) |
--outfile=file |
Output file (for a single entry point) |
--packages=external |
Keep all imported packages external |
--platform=browser|node|neutral |
Resolution/defaults platform (default browser) |
--serve=host:port |
Start a local HTTP dev server |
--sourcemap |
Emit a source map |
--splitting |
Enable code splitting (ESM only) |
--target=... |
Environment target (e.g. es2017, chrome58, node18) |
--watch |
Rebuild on file system changes |
Frequently used advanced options
| Flag | Description |
|---|---|
--alias:from=to |
Replace one package name with another |
--analyze / --analyze=verbose |
Print a bundle composition report |
--banner:js=... / --footer:js=... |
Prepend/append text to output of a given type (js/css) |
--charset=utf8 |
Don't escape UTF-8 code points in output |
--conditions=... |
package.json export conditions to honor |
--drop:console / --drop:debugger |
Remove console.* calls / debugger statements |
--inject:F |
Auto-import file F into every input file |
--jsx=automatic|transform|preserve |
JSX transform mode |
--keep-names |
Preserve original .name on functions and classes |
--legal-comments=... |
Where to place legal comments (none/inline/eof/linked/external) |
--log-level=... |
verbose/debug/info/warning/error/silent (default info) |
--mangle-props=regex |
Rename properties matching a regex |
--metafile=file |
Write build metadata JSON |
--out-extension:.js=.mjs |
Customize output file extension |
--public-path=... |
Base URL for the file loader |
--pure:N |
Mark N as a side-effect-free function for tree shaking |
--tree-shaking=true|false |
Force tree shaking on or off |
--tsconfig=file |
Use a specific tsconfig.json |
--watch=forever |
Watch even after stdin closes |
Note
Flags that can be specified once use
=(e.g.--outfile=x). Flags that can be specified multiple times use:(e.g.--define:K=V,--external:M,--loader:.ext=L). esbuild will suggest the correct form if you mix them up.
Environment variables
| Variable | Effect |
|---|---|
NO_COLOR |
Disable colored terminal output |
NODE_PATH |
Extra directories to resolve modules from (Node convention) |
ESBUILD_BINARY_PATH |
Override the path to the esbuild binary the JS API runs |
ESBUILD_WORKER_THREADS |
Set to 0 to disable the worker thread used by the sync APIs |
ESBUILD_MAX_BUFFER |
Max bytes of output the sync API will buffer (default 16 MB) |