| | |

esbuild 21 ⚡ The JavaScript API — esbuild.build() vs. CLI

⏱️ Estimated read: 12 min | |

The CLI and the JavaScript API are two interfaces to the same engine, but they are not interchangeable. The CLI is a process that parses arguments, runs a build, prints the result, and exits. The JavaScript API is a module that can be imported into a Node.js script, called with an options object, and used to orchestrate builds that the CLI cannot express. The documentation states the relationship plainly: the build() API “is the same as invoking the command-line tool” . The sameness is the baseline; the differences are the reasons the API exists.

The most consequential difference is the context API. Since esbuild 0.17.0, the watch and serve options were removed from build(), and the context() function was introduced . The context object holds the incremental state between builds, and the watch(), serve(), and rebuild() methods all operate on it. A build script that passes watch: true to build() fails on modern esbuild with an “Invalid option” error . The API is not a superset of the CLI; it is a different surface with a different lifecycle.

Key point: The CLI is for one-shot builds and simple development servers. The JavaScript API is for programmatic builds, custom file watchers, and integrations that need to call the build function repeatedly or inspect the result object. The context() function is the entry point for watch, serve, and rebuild; the build() function is the entry point for a single build.


Why the API exists separately from the CLI

The shell interpretation problem. The CLI arguments are interpreted by the shell before esbuild sees them. A glob pattern like *.json may be expanded by the shell into a list of filenames, which is not what the build expects. The documentation recommends single quotes or escaping, and it adds a stronger recommendation: “When in doubt, use the JavaScript or Go API instead of CLI” . The API receives the options as a JavaScript object, and no shell is involved. The entryPoints array is a list of strings, and the outdir is a string, and the values are exactly what the script passes.

The result object problem. The CLI prints a summary to the terminal. The API returns a BuildResult object that contains the errors, the warnings, the metafile, and the outputFiles . A script that needs to fail the process on an error, or analyze the bundle composition, or write the output to a custom location, uses the result object. The CLI prints and exits; the API returns and continues.

The incremental problem. A one-shot build reads the files, transforms them, and writes the output. A second build reads the files again. The context API caches the file contents and the parsed ASTs between builds, so the second build only re-processes the files that changed . The CLI’s --watch flag uses the same context internally, but the API exposes it. A custom file watcher—a build script that watches a directory outside the module graph, or a plugin that watches a configuration file—needs the context API, not the CLI flag.

The trade-off. The API is more verbose. A CLI command is one line; the equivalent API call is an options object with five or ten properties. The API requires a Node.js runtime, while the CLI is a standalone binary. The API’s result object is useful only if the script uses it; a simple build that writes to disk and exits does not need the object. The CLI is the right tool for the simple case, and the API is the right tool for the cases the CLI cannot express.


a. The CLI surface and the one-shot build

The CLI is the fastest way to run a build. The flags map to the BuildOptions properties, and the process exits with a code that indicates success or failure.

# A production bundle with minification and source maps.
# The flags map to the build options object.
esbuild src/app.ts --bundle --minify --sourcemap --outfile=dist/bundle.js

The CLI is the right choice for a build that runs once, writes the output, and exits. The flags are documented in the CLI reference, and the common ones are --bundle, --minify, --target, --platform, --outfile, and --outdir . The CLI is also the right choice for a quick transform of stdin:

# Transform a TypeScript string to JavaScript.
echo 'let x: number = 1' | esbuild --loader=ts

The stdin transform is a single operation with no file system access. The CLI is sufficient, and the API would add a process boundary for no benefit.


b. The API surface and the context lifecycle

The API has two entry points: build() for a one-shot build, and context() for a long-running context. The build() function returns a BuildResult and does not support watch or serve in modern esbuild.

// build.mjs — a one-shot build with the API
import * as esbuild from 'esbuild';

const result = await esbuild.build({
  entryPoints: ['src/app.ts'],
  bundle: true,
  minify: true,
  sourcemap: true,
  outfile: 'dist/bundle.js',
  // The metafile option produces a structured description of the build.
  // The CLI can write it with --metafile; the API returns it in the result.
  metafile: true,
});

// The result object contains the errors and warnings.
// The CLI would have printed them and exited; the API lets the script decide.
if (result.errors.length > 0) {
  console.error(result.errors);
  process.exit(1);
}

// The metafile is the bundle analysis data.
console.log(result.metafile.outputs);

The context() function is the entry point for the incremental cases. It returns a context object, and the watch(), serve(), and rebuild() methods are called on it.

// watch.mjs — a watch build with the context API
import * as esbuild from 'esbuild';

const ctx = await esbuild.context({
  entryPoints: ['src/app.ts'],
  bundle: true,
  outdir: 'dist',
});

// The initial build is not performed by context().
// The rebuild() call performs it, and the watch() call starts the watcher.
await ctx.rebuild();
await ctx.watch();

// The dispose() call stops the watcher and frees the resources.
process.on('SIGINT', async () => {
  await ctx.dispose();
  process.exit(0);
});

The context is the mechanism that the CLI’s --watch and --serve flags use internally. The API exposes it, which means a script can combine watch and serve on the same context, or call rebuild() manually from a custom file watcher . The CLI’s flags are a shorthand for the API’s methods, and the shorthand covers the common cases.


Complete Example Session

// ============================================
// PART 1: THE CLI ONE-SHOT BUILD
// ============================================

// The CLI is one line.
// The flags map to the build options.
// esbuild src/app.ts --bundle --minify --outfile=dist/bundle.js

// ============================================
// PART 2: THE API ONE-SHOT BUILD
// ============================================

// build.mjs
import * as esbuild from 'esbuild';

const result = await esbuild.build({
  entryPoints: ['src/app.ts'],
  bundle: true,
  minify: true,
  outfile: 'dist/bundle.js',
  metafile: true,
});

// The result object is the reason to use the API.
// The CLI would have printed the result and exited.
console.log(result.metafile.outputs);

// ============================================
// PART 3: THE RESULT OBJECT
// ============================================

// The BuildResult object contains:
// - errors: an array of error messages
// - warnings: an array of warning messages
// - metafile: the bundle analysis data (if the option is set)
// - outputFiles: the output file contents (if write: false is set)
//
// The CLI prints the errors and warnings and exits.
// The API returns them and lets the script decide.

// ============================================
// PART 4: THE CONTEXT API
// ============================================

// The context() function is the entry point for watch, serve, and rebuild.
// It replaces the removed watch and serve options.
import * as esbuild from 'esbuild';

const ctx = await esbuild.context({
  entryPoints: ['src/app.ts'],
  bundle: true,
  outdir: 'dist',
});

// The initial build.
await ctx.rebuild();

// The watch mode.
await ctx.watch();

// The cleanup.
await ctx.dispose();

// ============================================
// PART 5: THE SERVE MODE
// ============================================

// The serve mode starts a local HTTP server.
const ctx = await esbuild.context({
  entryPoints: ['src/app.ts'],
  bundle: true,
  outdir: 'www/js',
});

const { hosts, port } = await ctx.serve({
  servedir: 'www',
});

console.log(`Serving at http://${hosts[0]}:${port}`);

// ============================================
// PART 6: THE REBUILD MODE
// ============================================

// The rebuild mode is for custom file watchers.
// It is not available from the CLI.
import chokidar from 'chokidar';

const ctx = await esbuild.context({
  entryPoints: ['src/app.ts'],
  bundle: true,
  outdir: 'dist',
});

// The custom watcher calls rebuild() on each change.
chokidar.watch('src').on('all', async () => {
  const result = await ctx.rebuild();
  console.log(`Rebuilt at ${new Date().toISOString()}`);
});

// ============================================
// PART 7: THE TRANSFORM API
// ============================================

// The transform() API transforms a single file in memory.
// It is roughly equivalent to build() with bundle: false.
import * as esbuild from 'esbuild';

const result = await esbuild.transform('let x: number = 1', {
  loader: 'ts',
});

console.log(result.code); // "let x = 1;"

// ============================================
// PART 8: THE WATCH OPTION REMOVAL
// ============================================

// The watch option was removed from build() in esbuild 0.17.0.
// The old code fails:
// await esbuild.build({ watch: true }); // ❌ Invalid option
//
// The new code uses the context API:
const ctx = await esbuild.context({ /* options */ });
await ctx.watch();

// ============================================
// PART 9: THE SERVICE API (LEGACY)
// ============================================

// The old service API was replaced by the context API.
// The startService() function is no longer the recommended path.
// The context() function is the current API.

// ============================================
// PART 10: THE VERIFICATION CHECKLIST
// ============================================

// [ ] The one-shot build uses build(), not context().
// [ ] The watch build uses context() + watch().
// [ ] The serve build uses context() + serve().
// [ ] The rebuild build uses context() + rebuild().
// [ ] The result object is used if the script needs the errors or metafile.
// [ ] The context is disposed on exit.
// [ ] The CLI is used for simple one-shot builds.

The ten parts covered the CLI one-shot, the API one-shot, the result object, the context API, the serve mode, the rebuild mode, the transform API, the watch option removal, the service API, and the verification. The through-line is that the CLI and the API share the same engine but differ in lifecycle and result handling.


Quick Reference

The entry points

FunctionPurposeReturns
esbuild.build(options)One-shot buildPromise<BuildResult>
esbuild.context(options)Long-running contextPromise<BuildContext>
esbuild.transform(code, options)In-memory transformPromise<TransformResult>

The context methods

MethodPurpose
ctx.rebuild()Perform a build
ctx.watch()Start the file watcher
ctx.serve(options)Start the dev server
ctx.cancel()Cancel the current build
ctx.dispose()Free resources

The CLI vs API

AspectCLIAPI
InvocationShell commandNode.js script
ResultPrinted and exit codeBuildResult object
Watch--watch flagctx.watch()
Serve--serve flagctx.serve()
RebuildNot availablectx.rebuild()
Shell expansionPossibleNot applicable

Best Practices

✅ Do This:

// Use build() for a one-shot build
await esbuild.build({ entryPoints: ['app.ts'], bundle: true }); // ✅

// Use context() for watch and serve
const ctx = await esbuild.context({ /* ... */ }); await ctx.watch(); // ✅

// Use the result object for error handling
if (result.errors.length > 0) process.exit(1);                  // ✅

// Dispose the context on exit
await ctx.dispose();                                             // ✅

❌ Don’t Do This:

// Pass watch to build()
await esbuild.build({ watch: true }); // ❌ Invalid option          // ❌

// Use the CLI for a build that needs the metafile
// The CLI writes the file, the API returns the object.           // ❌

// Forget to dispose the context
// The watcher keeps the process alive.                           // ❌

Common Pitfalls

PitfallWhy It HappensFix
watch: true failsRemoved in 0.17.0Use context() + ctx.watch()
The build script exits immediatelyThe watcher is not keeping the process aliveDo not call process.exit() after watch()
The context is not disposedThe watcher holds resourcesCall ctx.dispose() on exit
The result is undefinedThe write: false option is neededSet write: false to get outputFiles

Real-World Examples

1. CLI one-shot

esbuild src/app.ts --bundle --minify --outfile=dist/bundle.js

2. API one-shot

await esbuild.build({ entryPoints: ['src/app.ts'], bundle: true, outfile: 'dist/bundle.js' });

3. Context watch

const ctx = await esbuild.context({ /* ... */ }); await ctx.watch();

4. Context serve

const { port } = await ctx.serve({ servedir: 'www' });

5. Context rebuild

await ctx.rebuild();

6. Transform

const result = await esbuild.transform('let x: number = 1', { loader: 'ts' });

7. Result errors

if (result.errors.length > 0) { /* handle */ }

8. Result metafile

console.log(result.metafile.outputs);

9. Dispose

await ctx.dispose();

10. The watch removal

// await esbuild.build({ watch: true }) // ❌
// const ctx = await esbuild.context({...}); await ctx.watch(); // ✅

Visual

The CLI vs API Flow

┌──────────────────────────────────────────────────────────────┐
│  CLI                          API                            │
│                                                              │
│  Shell parses args            Options object                 │
│         │                            │                       │
│         ▼                            ▼                       │
│  ┌─────────────┐              ┌─────────────┐               │
│  │ build()     │              │ build() or  │               │
│  │             │              │ context()   │               │
│  └──────┬──────┘              └──────┬──────┘               │
│         │                            │                       │
│         ▼                            ▼                       │
│  ┌─────────────┐              ┌─────────────┐               │
│  │ Print and   │              │ BuildResult │               │
│  │ exit        │              │ object      │               │
│  └─────────────┘              └─────────────┘               │
│                                                              │
│  The CLI is for the simple case.                            │
│  The API is for the cases that need the result or           │
│  the context.                                               │
└──────────────────────────────────────────────────────────────┘

The Context Lifecycle

┌──────────────────────────────────────────────────────────────┐
│  context() → rebuild() / watch() / serve() → dispose()        │
│                                                              │
│  const ctx = await esbuild.context(options)                 │
│         │                                                    │
│         ├──▶ await ctx.rebuild()    (manual build)          │
│         ├──▶ await ctx.watch()      (file watcher)          │
│         └──▶ await ctx.serve()      (dev server)            │
│                                                              │
│  await ctx.dispose()  (free resources)                      │
│                                                              │
│  The context holds the incremental state between builds.    │
│  The CLI's --watch and --serve flags use the same context.  │
└──────────────────────────────────────────────────────────────┘

The Watch Option Removal

┌──────────────────────────────────────────────────────────────┐
│  esbuild < 0.17                esbuild >= 0.17               │
│                                                              │
│  build({ watch: true })        context() + watch()           │
│  build({ serve: {} })          context() + serve()           │
│                                                              │
│  The old options were removed.                              │
│  The context API is the replacement.                        │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
One-shot buildesbuild.build(options)
Long-running contextesbuild.context(options)
Watchctx.watch()
Servectx.serve(options)
Rebuildctx.rebuild()
Cleanupctx.dispose()
Transformesbuild.transform(code, options)
CLI vs APICLI prints and exits; API returns a result object

Key takeaways:

  • The CLI and the API share the same engine. The build() function “is the same as invoking the command-line tool” . The flags map to the options object, and the build output is the same.
  • The result object is the reason to use the API. The CLI prints the errors and exits; the API returns a BuildResult with the errors, warnings, metafile, and outputFiles. A script that needs to inspect the result uses the API .
  • The context API is the incremental surface. The watch and serve options were removed from build() in 0.17.0. The context() function returns an object with watch(), serve(), and rebuild() methods .
  • The rebuild method is not available from the CLI. A custom file watcher that calls rebuild() on each change uses the context API. The CLI’s --watch flag is the shorthand for the common case .
  • The shell interpretation is a reason to use the API. The CLI arguments are interpreted by the shell before esbuild sees them. A glob pattern may be expanded unexpectedly. The API receives the options as a JavaScript object, and no shell is involved .

Remember: The CLI is a process that parses arguments, runs a build, prints the result, and exits. The API is a module that runs a build and returns the result. The two share the same engine, and the flags map to the options. The API’s value is the result object and the context lifecycle. The build() function is for the one-shot build. The context() function is for the watch, serve, and rebuild. The watch and serve options were removed from build(); the context is the replacement. The CLI is the right tool for the simple case. The API is the right tool for the cases that need the result or the context.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!