esbuild 05 ⚡ Output Formats — cjs, esm, and iife selection
The --format flag determines the module syntax that esbuild writes into the output file. It is not a cosmetic choice: the format decides whether the output uses require() and module.exports, import and export, or a self-executing function that attaches its exports to a global variable. The previous chapter covered the graph traversal and the tree shaking; this chapter covers the three formats that the graph can be emitted into, and the conditions that make each one the correct choice.
The three formats serve three different runtimes. The cjs format is for the Node.js environments that use require(). The esm format is for the modern browsers and the Node.js ESM, and it is the only format that preserves the tree shaking in the output. The iife format is for the browsers that load the script with a plain <script> tag and expect the library to attach itself to a global variable . The format is the contract between the bundle and the runtime, and the wrong format produces the “require is not defined” or the “Cannot use import statement outside a module” error.
Key point: The --format flag has no default when the bundling is disabled, and the default depends on the platform when the bundling is enabled. The iife is the default for the browser platform, and the cjs is the default for the Node platform . The esm format is the one that keeps the import and the export statements, which is the prerequisite for the tree shaking in the consumer’s bundler.
Why the output format requires the runtime match
The CommonJS problem.
The cjs format wraps the modules in the require() calls and the module.exports objects. It is the format that the older Node.js code expects, and it is the default for the --platform=node. A bundle in the cjs format cannot be loaded by the browser’s native ESM loader, because the browser does not have the require or the module globals . The cjs is the correct choice for the Node.js scripts, the CLI tools, and the packages that are consumed by the require() callers.
The ESM problem.
The esm format keeps the import and the export statements. It is the format for the modern browsers and the Node.js ESM, and it is the only format that allows the consumer’s bundler to tree-shake the bundle’s exports . A library published in the esm format can have its unused exports removed by the application that imports it, which is the mechanism that keeps the final application bundle small. The esm output file should have the .mjs extension (or the "type": "module" in the package.json) so that the Node.js treats it as ESM .
The IIFE problem.
The iife format wraps the entire bundle in a function that executes immediately, and it optionally assigns the exports to a global variable. It is the format for the <script src="..."> tags in the browsers that do not support the ESM, and for the libraries that are loaded through the global namespace . The --global-name flag specifies the name of the global variable, and the output becomes var MyLib = (function() { ... })(). The iife is the default for the --platform=browser, but it is not tree-shakeable by the consumer, because the exports are not the ESM exports.
a. The cjs format for Node.js
The cjs format is the default when the --platform=node is set, and it is the explicit choice for the Node.js scripts.
# Bundle as CommonJS for Node.js
npx esbuild src/index.ts --bundle --platform=node --format=cjs --outfile=dist/bundle.cjs
The --platform=node sets the format default to cjs and the target to the current Node.js version. The --format=cjs is explicit, and the .cjs extension reinforces the format. The output uses the require() for the external packages and the module.exports for the entry point’s exports . The cjs bundle is the correct choice for the CLI tools, the Node.js servers, and the packages that the require() consumers import.
The cjs format does not support the top-level await, because the CommonJS modules are not the async contexts. A bundle with the top-level await must use the esm format .
b. The esm format for the browsers and the tree shaking
The esm format is the explicit choice for the modern browsers and the Node.js ESM, and it is the format that preserves the tree shaking.
# Bundle as ESM for the modern browsers
npx esbuild src/index.ts --bundle --format=esm --outfile=dist/bundle.mjs
The --format=esm keeps the import and the export statements in the output. The .mjs extension tells the Node.js and the browsers that the file is an ESM module . The esm output can be imported by the <script type="module"> tags, and it can be consumed by the application’s bundler, which can tree-shake the unused exports . The esm is the correct choice for the libraries and the modern web applications.
The esm format is required for the code splitting. The esbuild’s code splitting only works with the esm output, because the dynamic import() produces the separate chunks that the ESM loader can fetch .
c. The iife format for the global scripts
The iife format wraps the bundle in a self-executing function and optionally attaches the exports to a global variable.
# Bundle as IIFE with a global name
npx esbuild src/index.ts --bundle --format=iife --global-name=MyLib --outfile=dist/bundle.js
The --format=iife is the default for the --platform=browser, and the --global-name=MyLib makes the exports available as window.MyLib . The output is var MyLib = (function() { ... return exports })(), which the <script src="dist/bundle.js"></script> tag loads, and the page can access the library through the MyLib global. The iife is the correct choice for the legacy browsers, the bookmarklets, and the scripts that must not rely on the module system.
The iife without the --global-name is the format for the entry point that has the side effects and does not need to export anything. The function executes, and the side effects happen, but the exports are not accessible .
Complete Example Session
# ============================================
# PART 1: THE CJS BUNDLE
# ============================================
npx esbuild src/index.ts --bundle --platform=node --outfile=dist/bundle.cjs
# The output uses the require() and the module.exports
# ============================================
# PART 2: THE CJS VERIFICATION
# ============================================
node dist/bundle.cjs
# The Node.js runs the bundle
# ============================================
# PART 3: THE ESM BUNDLE
# ============================================
npx esbuild src/index.ts --bundle --format=esm --outfile=dist/bundle.mjs
# The output keeps the import and the export
# ============================================
# PART 4: THE ESM VERIFICATION
# ============================================
node dist/bundle.mjs
# The Node.js runs the ESM bundle
# ============================================
# PART 5: THE IIFE BUNDLE
# ============================================
npx esbuild src/index.ts --bundle --format=iife --global-name=MyLib --outfile=dist/bundle.js
# The output assigns the exports to the global MyLib
# ============================================
# PART 6: THE IIFE VERIFICATION
# ============================================
grep "var MyLib" dist/bundle.js
# The global assignment is present in the output
# ============================================
# PART 7: THE FORMAT COMPARISON
# ============================================
# cjs: the require/module.exports, the Node.js only
# esm: the import/export, the browsers and the tree shaking
# iife: the global variable, the <script> tag
The seven parts demonstrate the format selection: the cjs bundle, the cjs verification, the esm bundle, the esm verification, the iife bundle, the iife verification, and the format comparison.
Quick Reference
The Formats
| Format | Syntax | Runtime | Tree Shakeable |
|---|---|---|---|
cjs | require() / module.exports | Node.js (legacy) | No |
esm | import / export | Browsers, Node.js ESM | Yes |
iife | The self-executing function + the global | The <script> tag | No |
The Defaults
| Platform | Default Format |
|---|---|
browser | iife |
node | cjs |
| No platform | No default (the bundling disabled) |
The Flags
| Flag | Purpose |
|---|---|
--format=cjs | The CommonJS output |
--format=esm | The ESM output |
--format=iife | The IIFE output |
--global-name=<name> | The global variable for the IIFE |
--outfile=<path> | The output file (the extension should match the format) |
The Extension Convention
| Format | Extension | The package.json |
|---|---|---|
cjs | .cjs | The default (or "type": "commonjs") |
esm | .mjs | The "type": "module" |
iife | .js | The <script> tag |
Best Practices
✅ Do This:
# Use the cjs for the Node.js scripts
npx esbuild src/index.ts --bundle --platform=node --outfile=dist/index.cjs # ✅
# Use the esm for the libraries and the modern web
npx esbuild src/index.ts --bundle --format=esm --outfile=dist/index.mjs # ✅
# Use the iife with the global name for the browser scripts
npx esbuild src/index.ts --bundle --format=iife --global-name=MyLib # ✅
# Match the extension to the format
# .cjs for the cjs, .mjs for the esm, .js for the iife # ✅
# Use the esm when the code splitting is needed
npx esbuild src/index.ts --bundle --format=esm --splitting # ✅
❌ Don’t Do This:
# Use the iife for the library that should be tree-shakeable
# The iife exports are not the ESM exports # ❌
# Use the cjs for the browser script
# The browser does not have the require() # ❌
# Use the esm with the .js extension and the type: commonjs
# The Node.js treats the .js as the CJS # ❌
# Use the top-level await in the cjs
# The CommonJS does not support the top-level await # ❌
# Forget the --global-name for the iife library
# The exports are not accessible # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The “require is not defined” | The cjs format in the browser | Use the esm or the iife |
| The “Cannot use import” | The esm in the CJS context | Use the cjs or the .mjs |
| The library is not tree-shakeable | The iife or the cjs output | Use the esm |
| The global is not defined | No --global-name | Add the flag |
| The code splitting fails | The format is not esm | Use the esm |
Real-World Examples
1. The cjs Node.js bundle
npx esbuild src/index.ts --bundle --platform=node --outfile=dist/index.cjs
2. The esm browser bundle
npx esbuild src/index.ts --bundle --format=esm --outfile=dist/index.mjs
3. The iife library
npx esbuild src/index.ts --bundle --format=iife --global-name=MyLib
4. The cjs with the .js extension
npx esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js
5. The esm with the .js extension
npx esbuild src/index.ts --bundle --format=esm --outfile=dist/index.js
6. The iife without the global
npx esbuild src/index.ts --bundle --format=iife --outfile=dist/index.js
7. The cjs with the external packages
npx esbuild src/index.ts --bundle --platform=node --packages=external
8. The esm with the code splitting
npx esbuild src/index.ts --bundle --format=esm --splitting --outdir=dist
9. The iife with the source map
npx esbuild src/index.ts --bundle --format=iife --sourcemap
10. The format check
grep "module.exports" dist/index.cjs # The cjs
grep "export" dist/index.mjs # The esm
grep "var MyLib" dist/index.js # The iife
Visual
The Three Formats
┌──────────────────────────────────────────────────────────────┐
│ THE FORMAT IS THE CONTRACT WITH THE RUNTIME │
│ │
│ cjs: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ const x = require('x') │ │
│ │ module.exports = { ... } │ │
│ │ → the Node.js legacy │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ esm: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ import { x } from 'x' │ │
│ │ export { ... } │ │
│ │ → the browsers, the Node.js ESM, the tree shaking │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ iife: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ var MyLib = (function() { ... return exports })() │ │
│ │ → the <script> tag, the global variable │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
The Default Selection
┌──────────────────────────────────────────────────────────────┐
│ THE PLATFORM DETERMINES THE DEFAULT FORMAT │
│ │
│ --platform=browser: │
│ └── the default format is the iife │
│ │
│ --platform=node: │
│ └── the default format is the cjs │
│ │
│ No platform + no bundle: │
│ └── no default; the format must be explicit │
│ │
│ The --format flag overrides the default. │
└──────────────────────────────────────────────────────────────┘
The Extension Convention
┌──────────────────────────────────────────────────────────────┐
│ THE EXTENSION TELLS THE RUNTIME HOW TO PARSE THE FILE │
│ │
│ .cjs → the CommonJS (the Node.js) │
│ .mjs → the ESM (the Node.js and the browsers) │
│ .js → the package.json "type" (the default commonjs) │
│ │
│ The esm output with the .js extension and no "type": module │
│ is parsed as the CommonJS, which breaks the import. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Formats | cjs, esm, iife |
cjs runtime | The Node.js legacy (the require) |
esm runtime | The browsers, the Node.js ESM, the tree shaking |
iife runtime | The <script> tag, the global variable |
Default for browser | iife |
Default for node | cjs |
| Default without the bundle | No default |
| Global name flag | --global-name for the iife |
| Extension | .cjs, .mjs, .js |
Key takeaways:
- The
cjsformat is for the Node.js legacy. It uses therequire()and themodule.exports, and it is the default for the--platform=node. It does not support the top-levelawait. - The
esmformat is for the modern browsers and the tree shaking. It keeps theimportand theexport, and it is the only format that allows the consumer’s bundler to remove the unused exports. It is required for the code splitting . - The
iifeformat is for the<script>tag. It wraps the bundle in the self-executing function and optionally assigns the exports to the global variable with the--global-name. It is the default for the--platform=browser. - The format must match the runtime. The
cjsin the browser produces the “require is not defined”. Theesmin the CommonJS context produces the “Cannot use import statement outside a module”. The format is the contract . - The extension reinforces the format. The
.cjsis the CommonJS, the.mjsis the ESM, and the.jsdepends on thepackage.json"type". Theesmoutput with the.jsextension and no"type": "module"is parsed as the CommonJS . - The
--platformflag sets the defaults. The--platform=browserdefaults to theiife, and the--platform=nodedefaults to thecjs. The--formatflag overrides the default when the explicit choice is needed .
Remember: The output format is the contract between the bundle and the runtime. The cjs is for the Node.js require, the esm is for the browsers and the tree shaking, and the iife is for the <script> tag and the global variable. The format must match the runtime, or the bundle fails to load. The extension and the package.json "type" reinforce the format. The result is the bundle that the target runtime can execute, which is the difference between the file that runs and the file that throws the “require is not defined”.
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!