Jesse A. Green / notebook

esbuild User Guide

Generated June 2026 from evanw/esbuild at 6a794df

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

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 a package.json script) instead of a global install. It guarantees every developer and CI machine uses the exact version pinned in your package.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 esbuild npm 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's optionalDependencies feature. This is why flags like --no-optional break 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 --version must match the version of the esbuild package. 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

--bundle is opt-in. Without it, esbuild transforms each entry point independently and leaves import/require statements in place. If your output still has bare import "./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 --noEmit separately 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 boolean true, 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 dynamic import() 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 file loader for large assets (they're copied and referenced by URL) and dataurl for small ones (inlined to avoid an extra HTTP request). The --public-path= flag sets the base URL prepended to file-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() or context()). They cannot be used with buildSync() / 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(), and analyzeMetafile() are available. The *Sync variants throw — WebAssembly has no synchronous child process. build() works but cannot write to the file system; use write: false and read outputFiles.


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)