Search across all documentation pages
require)CommonJS (require, module.exports) powered Node for years - you still maintain it in legacy packages, config loaders, and .cjs shims while new services standardize on ESM.
// config.cjs
const path = require('node:path');
module.exports = {
port: Number(process.env.PORT ?? 3000),
root: __dirname,
resolve: (p) => path.join(__dirname, p),
};const config = require('./config.cjs');When to reach for this:
require in brownfield repos.cjs tooling hooks (jest.config.cjs, prettier.config.cjs)createRequire bridges from ESM entry points// circular-a.cjs
const b = require('./circular-b.cjs');
module.exports = {
name: 'A',
bName: () => b.name,
};// circular-b.cjs
const a = require('./circular-a.cjs');
module.exports = {
name: 'B',
aName: () => a.name,
};// main.cjs
const a = require('./circular-a.cjs');
console.log(a.name, a.bName()); // A B - partial exports during circular init// esm-dirname-polyfill.cjs - pattern ESM importers avoid by using import.meta.url
const { fileURLToPath } = require('node:url');
// Only needed when teaching CJS vs ESM differencesWhat this demonstrates:
require returns module.exports - assignments replace the whole export objectmodule.exports until modules finish initializing__dirname is the directory of the current CJS file - no ESM equivalent without import.meta.urlrequire('./same') returns the same object referencemodule.exports in require.cache.exports.foo = 1 is sugar for module.exports.foo until you reassign module.exports = ....require('node:fs') does not hit disk.require('./x.json') in CJS - ESM needs readFile + JSON.parse or import assertions patterns.| Pattern | Result |
|---|---|
module.exports = fn | Default export equivalent |
exports.a = 1 | Named property on exports |
module.exports = { a, b } | Single object export |
// consume CJS from TS with esModuleInterop
import config from './config.cjs';
// or
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const cfg = require('./config.cjs') asexports after module.exports = - breaks. Fix: only mutate module.exports once.require inside functions or refactor shared module.__dirname in ESM - ReferenceError. Fix: import.meta.url + fileURLToPath.../../../ paths - fragile moves. Fix: migrate to ESM with exports map or path aliases in tsconfig.| Alternative | Use When | Don't Use When |
|---|---|---|
ESM import | All new application code | Unsupported legacy test runner without config |
createRequire | ESM app needs one CJS dep | Entire package can be ESM |
Dynamic import() | Async CJS interop from ESM | Sync config load at top of CJS |
JSON module import | Static config in ESM | Need hot-reload without restart |
Not removed, but ESM is the forward path. CommonJS remains for compatibility and .cjs config.
Not directly. Use createRequire(import.meta.url) for exceptions.
Object keyed by resolved paths - delete entries for rare hot-reload in dev tools.
Node returns incomplete module.exports until the module body finishes - design to avoid cycles.
Not natively - compile to JS first or use tsx/ts-node loaders in dev.
Forces CommonJS parsing when package "type": "module".
Generally no from CJS - ESM is async. Import ESM from ESM or use dynamic import bridge.
module.exports = function myFn() {} or module.exports = { myFn }.
Initially exports references module.exports. Reassigning module.exports breaks the alias.
Library authors support both ecosystems via exports conditions - consumers pick via import style.
Nest supports both; new projects increasingly use ESM with SWC - follow team ADR.
Rename to .cjs only where needed, convert leaf modules first, then use createRequire temporarily at boundaries.
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.