Node.js Build Tooling & Bundlers
Parent: JavaScript and Node.js · researched 2026-06-02T22:05:54.259Z· 19 sources · 8 concepts · skill nodejs-build-tooling-bundlers
This reference is about **turning Node.js + TypeScript source into a production
Overview
- This reference is about **turning Node.js + TypeScript source into a production [source]
- artifact** - a bundled server, a single-file CLI, a Lambda zip, or a publishable [source]
- library - and picking the tool that fits each shape. It is the build-time companion [source]
- to three sibling references that own adjacent layers: [source]
- nodejs-typescript-and-runtime-features - runtime TypeScript: native [source]
- type-stripping and dev runners (tsx, ts-node) that execute .ts directly. This [source]
- file covers tsx only at the boundary - "dev runner vs prod bundler." [source]
- nodejs-module-resolution - the resolution algorithm and package.json [source]
- "exports"/conditions. This file reads those fields but does not re-derive them. [source]
- nodejs-package-management-supply-chain / devops-containers-cicd — [source]
- npm/pnpm install, lockfiles, publishing/provenance. This file produces the [source]
- artifact; those own how it is installed and shipped. [source]
- The mental model: most Node backends do not need a bundler at all. Reach for one [source]
- only when single-file packaging, startup-time/cold-start, or library output quality [source]
- justifies it. Then choose by output shape: esbuild/swc to transpile fast, [source]
- tsup for a dual-format library with types, Rollup for the cleanest library [source]
- bundle, @vercel/ncc to collapse everything into one file. [source]
1. The bundle-vs-ship-source decision for Node backends
- Unlike the browser (where every byte is downloaded), a Node backend already has the [source]
- files on disk, so bundling is optional and situational. Ship **source + [source]
- node_modules** for a normal long-lived server in a container: simplest path, honest [source]
- stack traces, native addons resolve normally. Bundle when you need: [source]
- A single distributable file - a CLI published to npm (smaller install, fewer [source]
- files), a GitHub Action, or a Lambda/Edge artifact that must be self-contained. [source]
- Faster cold starts - serverless functions pay per-file I/O at init; one [source]
- pre-resolved file with dead-code elimination (DCE) reduces parse/resolve cost. [source]
- A library with multiple output formats (ESM + CJS) and bundled internal modules. [source]
- Tree-shaking / DCE statically drop unused exports; they work on ES module [source]
- syntax (import/export), not CommonJS require, which is why ESM input matters. [source]
- Minification (whitespace + identifier renaming + syntax compression) shrinks bytes [source]
- — useful for libraries and Lambda size limits, rarely worth the debugging cost for a [source]
- plain server. Key caveat: keep native addons (.node), workers, and dynamic [source]
- require paths external - bundlers can't trace them. [source]
2. esbuild — the fast default
- esbuild is a Go-based bundler/transpiler whose draw is [source]
- raw speed. Two entry points: [source]
- Transform API (esbuild.transform(code, opts)) - processes a single in-memory [source]
- string "in an isolated environment that's completely disconnected from any other [source]
- files." No bundling, no plugins. Use it to transpile one file (TS→JS) in a pipeline. [source]
- Build API (esbuild.build(opts)) - the primary interface: reads entryPoints, [source]
- follows imports, writes to outfile/outdir. Supports bundle, plugins, watch/rebuild. [source]
- Node-relevant options: [source]
- platform: 'node' - sets format to cjs, marks Node built-ins external, and [source]
- adds the node export condition. ('browser' → iife; 'neutral' → esm.) [source]
- format - 'esm' | 'cjs' | 'iife'. Override the platform default explicitly [source]
- for ESM output on Node (.mjs or "type":"module"). [source]
- bundle: true - inline imported deps (off by default). [source]
- **external: ['pg', '*.node'] and packages: 'external'** - the latter marks [source]
- all dependencies external (the common server recipe: bundle your code, leave [source]
- node_modules on disk). [source]
- minify, treeShaking (on by default when bundling; honors package.json [source]
- sideEffects), target: 'node20', sourcemap (true/'inline'/'external'). [source]
- Hard limit - no type-checking. esbuild strips types; per the docs, *"esbuild does [source]
- not do any type checking so you will still need to run tsc --noEmit in parallel."* [source]
- It also never emits .d.ts. Enable isolatedModules in tsconfig.json because each [source]
- file is compiled independently. It honors experimentalDecorators but not [source]
- emitDecoratorMetadata (that needs the type system). [source]
3. swc — Rust-speed transpilation
- SWC ("Speedy Web Compiler") is a Rust-based platform for compiling [source]
- TS/JS. @swc/core exposes transform / transformSync / transformFile (plus [source]
- minify and parse); it is "mainly useful for build-tool authors." Configured by [source]
- .swcrc (or inline jsc): [source]
- jsc.parser.syntax = "typescript" | "ecmascript", with tsx/jsx and [source]
- decorators flags. [source]
- jsc.target (e.g. "es2022"), jsc.transform.legacyDecorator / [source]
- decoratorMetadata - the SWC equivalents that do support [source]
- emitDecoratorMetadata, which is why NestJS/TypeORM stacks favor swc over esbuild. [source]
- module.type = "commonjs" | "es6" | "umd" | "amd"; minify: true. [source]
- swc vs esbuild: both are far faster than Babel/tsc and both skip type-checking. [source]
- esbuild is also a bundler; swc is primarily a compiler/transform (bundling via the [source]
- separate, less-used @swc/pack). Pick swc for decorator metadata or its ecosystem [source]
- (Next.js, Jest via @swc/jest); pick esbuild when you want one tool that also [source]
- bundles. The dev-time runtime loader @swc-node/register belongs to [source]
- nodejs-typescript-and-runtime-features. [source]
4. tsup — the library-build sweet spot
- tsup is "the simplest and fastest way to bundle your [source]
- TypeScript libraries," an esbuild wrapper that adds the two things esbuild lacks for [source]
- libraries: easy dual-format output and .d.ts generation. Zero-config defaults [source]
- plus tsup.config.ts: [source]
- entry (entry points), format: ['esm', 'cjs'] (or --format esm,cjs) → [source]
- emits both .js/.mjs + .cjs so one package serves ESM and CJS consumers. [source]
- dts: true (or --dts) → generates a bundled .d.ts (delegates to the TS [source]
- compiler) - the feature that makes it a library tool, not just a transpiler. [source]
- target, minify, sourcemap, splitting (code-splitting, ESM [source]
- only), treeshake, --watch, --no-bundle (transpile-only mode). [source]
- Note: tsup's README now points to tsdown (a Rolldown-based successor) as the [source]
- recommended direction with a migration guide; tsup remains widely used and the patterns [source]
- here transfer. Use tsup (or tsdown) for a publishable package; for an application [source]
- you usually want a plain esbuild build or no bundle at all. [source]
5. Rollup — when output quality and plugins matter
- Rollup "compiles small pieces of code into something larger, [source]
- such as a library or application," and it pioneered tree-shaking ("statically [source]
- analyzes the code you are importing, and will exclude anything that isn't actually [source]
- used" - "more effective than simply running an automated minifier"). Reach for Rollup [source]
- You want the cleanest library bundle - Rollup's output is famously readable and [source]
- flat, with the best tree-shaking; many published packages are built with it. [source]
- You need output formats beyond esbuild's set: es, cjs, umd, [source]
- iife, amd, system (UMD/AMD/SystemJS matter for some consumers). [source]
- You need its plugin ecosystem (@rollup/plugin-node-resolve, [source]
- @rollup/plugin-commonjs, @rollup/plugin-typescript) or code-splitting with [source]
- precise control. The trade-off is speed: Rollup is slower than esbuild/swc. [source]
- (Rolldown - a Rust port of Rollup - and tsdown are the emerging fast successors.) [source]
6. @vercel/ncc — single-file compilation
- @vercel/ncc compiles "a Node.js module into a single [source]
- file, together with all its dependencies, gcc-style." Built on webpack under the hood, [source]
- it does static analysis to relocate assets and handles binary addons and dynamic [source]
- requires better than a naive bundle. The canonical use cases are exactly the [source]
- self-contained ones: CLIs, GitHub Actions (a committed dist/index.js), and [source]
- It handles TypeScript natively. Choose ncc when the deliverable is *"one file, drop it [source]
- anywhere, no node_modules"*; choose esbuild/tsup when you want speed or library [source]
- formats and are willing to keep some deps external. [source]
7. Source maps for Node + tsconfig path-alias resolution
- Two production-correctness concerns that bite bundled/transpiled Node code: [source]
- Source maps. Generate them in the bundler (sourcemap: true / --sourcemap / [source]
- -s), then run Node with --enable-source-maps (or NODE_OPTIONS) so traces [source]
- "report stack traces relative to the original source file." Caveat from the docs: it [source]
- "can introduce latency... when Error.stack is accessed" - fine for most servers, [source]
- note it for hot error paths. If you override Error.prepareStackTrace, call the [source]
- original to preserve mapping. [source]
- TS path aliases (compilerOptions.paths) in bundles. tsc rewrites nothing at [source]
- runtime, so @app/* aliases break unless the bundler resolves them. esbuild reads [source]
- paths from tsconfig.json - but only when --bundle is set (in transpile-only [source]
- mode the alias survives into output and fails at runtime). For non-bundling builds use [source]
- esbuild's alias option, a plugin (esbuild-plugin-tsconfig-paths), or a runtime [source]
- resolver. Rollup uses @rollup/plugin-alias / rollup-plugin-typescript-paths. [source]
8. tsx / native TS for dev vs a bundler for prod
- tsx ("TypeScript Execute") runs .ts directly in Node, powered by esbuild as a [source]
- transpiler, not a bundler. It is a dev/script runner (watch mode, zero-config, [source]
- no installation via npx tsx) and, like esbuild, does not type-check - it lets you [source]
- run code without being blocked by type errors. The decision rule: **tsx (or Node's [source]
- native type-stripping) for development and one-off scripts; a real bundler/build step [source]
- for production.** Don't ship a server by running tsx in prod - produce a built [source]
- artifact and run plain node. (Deep runtime-loader internals → the runtime-features [source]
Practical patterns
- Server recipe (esbuild): `bundle: true, platform: 'node', format: 'esm', [source]
- target: 'node20', packages: 'external', sourcemap: true` → one entry file, deps stay [source]
- in node_modules; run with node --enable-source-maps dist/index.js. [source]
- Lambda / single-file recipe: drop packages: 'external' so deps are inlined (or [source]
- use ncc build), add minify: true, keep only true natives external. Smaller cold [source]
- start, self-contained zip. [source]
- Library recipe (tsup): `entry: ['src/index.ts'], format: ['esm','cjs'], [source]
- dts: true, sourcemap: true, treeshake: true, and wire package.json exports` to [source]
- the emitted ESM/CJS/.d.ts (resolution details → nodejs-module-resolution). [source]
- Always pair a fast transpiler with a type gate - esbuild/swc/tsx skip types, so [source]
- run tsc --noEmit (or tsc -p tsconfig.build.json --emitDeclarationOnly for types) [source]
- in CI alongside the build. Speed for builds, tsc for correctness. [source]
- Set isolatedModules: true in any project transpiled file-by-file (esbuild/swc/ [source]
- tsx/Babel) so you catch unsafe cross-file type re-exports at design time. [source]
Anti-patterns
- Bundling a normal long-lived server "for performance." A containerized server [source]
- rarely benefits; you trade simpler stack traces and native-addon resolution for [source]
- little. Bundle for packaging (CLI/Lambda/Action) or cold start, not by reflex. [source]
- Trusting a fast transpiler to catch type errors. esbuild/swc/tsx emit happily on [source]
- broken types. No tsc --noEmit in CI = type safety lost. [source]
- Shipping a bundle with no source maps, or generating maps but forgetting [source]
- --enable-source-maps - every prod stack trace points at minified output. [source]
- Inlining native addons / dynamic require targets. Bundlers can't trace .node [source]
- files or runtime-computed paths; mark them external or the artifact crashes at load. [source]
- Expecting tsconfig paths to "just work." They only resolve when the bundler [source]
- is told to (esbuild needs --bundle; otherwise add a plugin/alias) - easy silent [source]
- ERR_MODULE_NOT_FOUND in production. [source]
- Running tsx/ts-node as your production process. Per-request transpile cost and [source]
- no build artifact; build once, run node. [source]
Troubleshooting
- ERR_REQUIRE_ESM / "exports is not defined" at runtime → format mismatch. The [source]
- bundle is ESM but loaded as CJS (or vice-versa); set format to match package.json [source]
- "type" and the file extension (.mjs/.cjs). [source]
- Stack traces point at bundled/minified code → you didn't run with [source]
- --enable-source-maps, or the bundler didn't emit a map (sourcemap: true). [source]
- Cannot find module '@app/...' → tsconfig paths weren't resolved at build; [source]
- enable --bundle (esbuild), add a paths plugin, or use alias. [source]
- Decorator metadata missing (DI fails in Nest/TypeORM) → esbuild ignores [source]
- emitDecoratorMetadata; switch that build to swc (decoratorMetadata: true) or tsc. [source]
- No .d.ts in the published package → esbuild/swc never emit declarations; use [source]
- tsup --dts or run tsc --emitDeclarationOnly. [source]
- Bundle crashes loading a native addon → mark it (and *.node) external; native [source]
- binaries can't be inlined. [source]
- Type error slipped to production → the fast transpiler doesn't type-check; add a [source]
- tsc --noEmit step to CI. [source]
References
- Bundle-vs-ship-source, tree-shaking & DCE (Node backends) [source]
- esbuild - bundling & the bundle/packages options: https://esbuild.github.io/api/ [source]
- Rollup - what it is + tree-shaking definition: https://rollupjs.org/introduction/ [source]
- Node.js - CLI (--enable-source-maps, runtime flags for built artifacts): https://nodejs.org/api/cli.html [source]
- esbuild - API (transform vs build, platform, format, external, packages, minify, sourcemap, tree shaking): https://esbuild.github.io/api/ [source]
- esbuild - Getting started (build/transform examples): https://esbuild.github.io/getting-started/ [source]
- esbuild - Content types / TypeScript (no type-checking → tsc --noEmit, isolatedModules, decorators): https://esbuild.github.io/content-types/ [source]
- SWC - Getting started & overview: https://swc.rs/docs/getting-started [source]
- SWC - .swcrc configuration (jsc.parser, target, transform, module, minify): https://swc.rs/docs/configuration/swcrc [source]
- SWC - @swc/core usage (transform/transformSync/transformFile): https://swc.rs/docs/usage/core [source]
- tsup - docs (esbuild-powered, format, dts, entry, watch): https://tsup.egoist.dev/ [source]
- tsup - README / repo (dual ESM+CJS, --dts, tsdown successor note): https://github.com/egoist/tsup [source]
- esbuild - API (the engine tsup wraps): https://esbuild.github.io/api/ [source]
- Rollup - Introduction (output formats es/cjs/umd/iife/amd/system, tree-shaking, libraries): https://rollupjs.org/introduction/ [source]
- Rollup - Configuration options (output.format, code-splitting): https://rollupjs.org/configuration-options/ [source]
- esbuild - API (speed/feature contrast for the when-Rollup-over-esbuild call): https://esbuild.github.io/api/ [source]
- @vercel/ncc - repo (single-file compile, ncc build, -m/-s/-e/-w, assets/addons): https://github.com/vercel/ncc [source]
- Node.js - CLI (running the produced single file): https://nodejs.org/api/cli.html [source]
- esbuild - API (alternative bundler for the same single-file goal): https://esbuild.github.io/api/ [source]
- Source maps for Node + tsconfig path-alias resolution [source]
- Node.js - --enable-source-maps (stack traces to original source, Error.stack latency): https://nodejs.org/api/cli.html#--enable-source-maps [source]
- esbuild - API (sourcemap, alias, reading tsconfig paths under --bundle): https://esbuild.github.io/api/ [source]
- esbuild-plugin-tsconfig-paths (alias resolution when not bundling): https://www.npmjs.com/package/esbuild-plugin-tsconfig-paths [source]
- tsx / native TS for dev vs bundler for prod [source]
- tsx - site (run TS directly, esbuild-powered, dev runner): https://tsx.is/ [source]
- tsx - FAQ / TypeScript (no type-checking; transpiler not bundler): https://tsx.is/faq [source]
- esbuild - Content types (the no-type-check engine behavior tsx inherits): https://esbuild.github.io/content-types/ [source]
Children
- The bundle-vs-ship-source decision for Node backends (tree-shaking, DCE, minification) (frontier)
- esbuild (transform vs build API, platform:node, format, external/packages, no type-checking) (frontier)
- swc (@swc/core, .swcrc, Rust speed, decorators/metadata, swc vs esbuild) (frontier)
- tsup (esbuild wrapper, dual ESM+CJS, .d.ts generation, the library-build sweet spot) (frontier)
- Rollup (output formats, tree-shaking, code-splitting, plugin ecosystem, when over esbuild) (frontier)
- @vercel/ncc (single-file compilation for CLIs/Actions/Lambda) (frontier)
- Source maps for Node (--enable-source-maps) + tsconfig path-alias resolution in bundles (frontier)
- tsx / native TS for dev vs a bundler for prod (frontier)
Frontier under this node: @vercel/ncc (single-file compilation for CLIs/Actions/Lambda), Rollup (output formats, tree-shaking, code-splitting, plugin ecosystem, when over esbuild), Source maps for Node (--enable-source-maps) + tsconfig path-alias resolution in bundles, The bundle-vs-ship-source decision for Node backends (tree-shaking, DCE, minification), esbuild (transform vs build API, platform:node, format, external/packages, no type-checking), swc (@swc/core, .swcrc, Rust speed, decorators/metadata, swc vs esbuild), tsup (esbuild wrapper, dual ESM+CJS, .d.ts generation, the library-build sweet spot), tsx / native TS for dev vs a bundler for prod