CommonJS (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.
Recipe
// 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:
- Maintaining older npm dependencies that export CJS only
- Jest configuration files still on
requirein brownfield repos .cjstooling hooks (jest.config.cjs,prettier.config.cjs)createRequirebridges from ESM entry points
Working Example
// 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:
requirereturnsmodule.exports- assignments replace the whole export object- Circular requires return partial
module.exportsuntil modules finish initializing __dirnameis the directory of the current CJS file - no ESM equivalent withoutimport.meta.url- Cache: second
require('./same')returns the same object reference
Deep Dive
How It Works
- Node resolves the path, wraps the file in a function, and caches
module.exportsinrequire.cache. exports.foo = 1is sugar formodule.exports.foountil you reassignmodule.exports = ....- Builtin modules load first -
require('node:fs')does not hit disk. - JSON can be
require('./x.json')in CJS - ESM needsreadFile+JSON.parseor import assertions patterns.
exports Patterns
| Pattern | Result |
|---|---|
module.exports = fn | Default export equivalent |
exports.a = 1 | Named property on exports |
module.exports = { a, b } | Single object export |
TypeScript Notes
// 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') as { port: number };Gotchas
- Reassigning
exportsaftermodule.exports =- breaks. Fix: only mutatemodule.exportsonce. - Circular dependency surprises - undefined imports mid-init. Fix: lazy
requireinside functions or refactor shared module. - Assuming
__dirnamein ESM - ReferenceError. Fix:import.meta.url+fileURLToPath. - Mutating required singletons - all importers see changes. Fix: treat exports as shared state intentionally or freeze.
- Deep
../../../paths - fragile moves. Fix: migrate to ESM withexportsmap or path aliases intsconfig.
Alternatives
| 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 |
FAQs
Is require deprecated?
Not removed, but ESM is the forward path. CommonJS remains for compatibility and .cjs config.
Can I use require in ESM?
Not directly. Use createRequire(import.meta.url) for exceptions.
What is require.cache?
Object keyed by resolved paths - delete entries for rare hot-reload in dev tools.
How do circular deps resolve?
Node returns incomplete module.exports until the module body finishes - design to avoid cycles.
Does require work with TypeScript files?
Not natively - compile to JS first or use tsx/ts-node loaders in dev.
What is .cjs extension?
Forces CommonJS parsing when package "type": "module".
Can I require ESM modules?
Generally no from CJS - ESM is async. Import ESM from ESM or use dynamic import bridge.
How do I export a function in CJS?
module.exports = function myFn() {} or module.exports = { myFn }.
Is module.exports === exports?
Initially exports references module.exports. Reassigning module.exports breaks the alias.
Why do some packages ship dual CJS/ESM?
Library authors support both ecosystems via exports conditions - consumers pick via import style.
Should new NestJS 11 apps use CJS?
Nest supports both; new projects increasingly use ESM with SWC - follow team ADR.
How do I migrate require to import?
Rename to .cjs only where needed, convert leaf modules first, then use createRequire temporarily at boundaries.
Related
- ES Modules (import) - the forward path
- CJS ↔ ESM Interop - bridging strategies
- package.json type & exports - package boundaries
- Modules Basics - side-by-side intro
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.