| | |

esbuild 05 ⚡ Output Formats — cjs, esm, and iife selection

⏱️ Estimated read: 12 min | |

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

FormatSyntaxRuntimeTree Shakeable
cjsrequire() / module.exportsNode.js (legacy)No
esmimport / exportBrowsers, Node.js ESMYes
iifeThe self-executing function + the globalThe <script> tagNo

The Defaults

PlatformDefault Format
browseriife
nodecjs
No platformNo default (the bundling disabled)

The Flags

FlagPurpose
--format=cjsThe CommonJS output
--format=esmThe ESM output
--format=iifeThe 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

FormatExtensionThe package.json
cjs.cjsThe default (or "type": "commonjs")
esm.mjsThe "type": "module"
iife.jsThe <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

PitfallWhy It HappensFix
The “require is not defined”The cjs format in the browserUse the esm or the iife
The “Cannot use import”The esm in the CJS contextUse the cjs or the .mjs
The library is not tree-shakeableThe iife or the cjs outputUse the esm
The global is not definedNo --global-nameAdd the flag
The code splitting failsThe format is not esmUse 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

ItemValue
Formatscjs, esm, iife
cjs runtimeThe Node.js legacy (the require)
esm runtimeThe browsers, the Node.js ESM, the tree shaking
iife runtimeThe <script> tag, the global variable
Default for browseriife
Default for nodecjs
Default without the bundleNo default
Global name flag--global-name for the iife
Extension.cjs, .mjs, .js

Key takeaways:

  • The cjs format is for the Node.js legacy. It uses the require() and the module.exports, and it is the default for the --platform=node. It does not support the top-level await .
  • The esm format is for the modern browsers and the tree shaking. It keeps the import and the export, 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 iife format 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 cjs in the browser produces the “require is not defined”. The esm in the CommonJS context produces the “Cannot use import statement outside a module”. The format is the contract .
  • The extension reinforces the format. The .cjs is the CommonJS, the .mjs is the ESM, and the .js depends on the package.json "type". The esm output with the .js extension and no "type": "module" is parsed as the CommonJS .
  • The --platform flag sets the defaults. The --platform=browser defaults to the iife, and the --platform=node defaults to the cjs. The --format flag 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”.


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!