Every file in a Node.js application is a module — an isolated unit of code with its own scope. The module system controls how code is shared between files: what one file exports, another can import. Node.js has two module systems: CommonJS (CJS) — the original Node.js module format using require() and module.exports — and ES Modules (ESM) — the official JavaScript standard using import and export. Understanding both, when to use each, and how they interact is critical because the MEAN Stack ecosystem is in the middle of a migration, and you will encounter both systems in real projects.
CommonJS vs ES Modules — Side by Side
| Feature | CommonJS (CJS) | ES Modules (ESM) |
|---|---|---|
| Export syntax | module.exports = ... or exports.fn = ... |
export default ... or export const fn = ... |
| Import syntax | const x = require('./module') |
import x from './module.js' |
| Loading | Synchronous — executes immediately | Asynchronous — parsed statically |
| File extension | .js or .cjs |
.mjs or .js with "type":"module" in package.json |
| Top-level await | Not supported | Supported |
| Tree-shaking | Not possible — dynamic | Possible — static analysis |
__dirname |
Available | Not available — use import.meta.url |
require() |
Available | Not available (use createRequire) |
| Named exports | Via exports.name = ... |
Via export const name = ... |
| Default export | module.exports = value |
export default value |
| Circular deps | Partially supported — may get incomplete exports | Better handling via live bindings |
| Ecosystem support | Universal — all Node.js packages | Growing — most major packages now support |
When to Use Which
| Scenario | Recommended | Reason |
|---|---|---|
| New Node.js project (2025+) | ESM | Future standard, top-level await, tree-shakeable |
| Existing Express/Node project | CJS | Avoid migration cost — CJS works perfectly |
| npm package for others | Dual CJS + ESM | Support both consumers |
| Angular / frontend | ESM always | Angular uses TypeScript with ESM import/export |
| Config files (jest.config, etc.) | CJS | Many tools still require CJS config files |
"type": "module" to package.json. All .js files in the project are then treated as ESM. If you need some files to remain CommonJS, use the .cjs extension for them. Conversely, you can use ESM files in a CJS project by giving them the .mjs extension. The simplest rule: pick one format per project and be consistent.import { handler } from './auth' fails in Node.js ESM — you must write import { handler } from './auth.js'. This differs from TypeScript where extensions are omitted. When using TypeScript with Node.js ESM, write import { handler } from './auth.js' in your .ts files — TypeScript understands this and resolves to auth.ts.require() an ES Module from a CommonJS file — it will throw a ERR_REQUIRE_ESM error. You can, however, use dynamic import() from CommonJS to load an ESM module asynchronously: const { fn } = await import('./esm-module.js'). This is the correct interop path when you must mix the two systems.Basic Example
// ══════════════════════════════════════════════════════
// COMMONJS — the traditional Node.js module system
// ══════════════════════════════════════════════════════
// math.js — CommonJS exports
const PI = 3.14159;
function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }
function multiply(a, b) { return a * b; }
// Export an object (named exports)
module.exports = { add, subtract, multiply, PI };
// OR export individually via exports shorthand
exports.add = add;
exports.subtract = subtract;
// Note: never mix `module.exports = {...}` with `exports.x = ...` — module.exports wins
// app.js — CommonJS imports
const math = require('./math'); // import entire module
const { add } = require('./math'); // destructure named export
console.log(math.add(2, 3)); // 5
console.log(add(2, 3)); // 5
console.log(require('./math').PI); // 3.14159
// Require a built-in module
const path = require('path');
// Require an npm package
const express = require('express');
// ── CJS Module Caching ────────────────────────────────────────────────────
// require() caches modules — the same instance is returned every time
const configA = require('./config');
const configB = require('./config');
console.log(configA === configB); // true — same object reference
// ══════════════════════════════════════════════════════
// ES MODULES — the modern JavaScript standard
// ══════════════════════════════════════════════════════
// math.mjs — ES Module exports
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export default class Calculator {
add(a, b) { return a + b; }
}
// app.mjs — ES Module imports
import Calculator, { add, PI } from './math.mjs'; // default + named
import * as math from './math.mjs'; // import all as namespace
console.log(add(2, 3)); // 5
console.log(math.PI); // 3.14159
const calc = new Calculator();
// Dynamic import — lazy load (works in both CJS and ESM)
const { default: Calculator2 } = await import('./math.mjs');
// ── __dirname equivalent in ESM ───────────────────────────────────────────
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const configPath = join(__dirname, 'config', 'settings.json');
// ── Top-level await in ESM (not available in CJS) ─────────────────────────
import { readFile } from 'fs/promises';
const config = JSON.parse(await readFile('./config.json', 'utf8'));
// No need to wrap in an async IIFE — top-level await works natively in ESM
Configuring a Node.js Project for ESM
// package.json — enable ES Modules for all .js files
{
"name": "my-app",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node src/index.js",
"dev": "node --watch src/index.js"
}
}
// src/index.js — ESM entry point (type: module in package.json)
import express from 'express';
import { connect } from 'mongoose';
import { readFile } from 'fs/promises';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
// Top-level await — load config before starting server
const config = JSON.parse(
await readFile(join(__dirname, '..', '.env.json'), 'utf8')
);
const app = express();
await connect(config.mongoUri);
app.listen(config.port, () => console.log(`Server on port ${config.port}`));
Real-World Example: Feature Module Pattern
// ── CommonJS — Feature Module Structure (typical Express project) ─────────
// features/tasks/task.model.js
const mongoose = require('mongoose');
const taskSchema = new mongoose.Schema({
title: { type: String, required: true },
completed: { type: Boolean, default: false },
user: { type: mongoose.Types.ObjectId, ref: 'User' },
}, { timestamps: true });
module.exports = mongoose.model('Task', taskSchema);
// features/tasks/task.service.js
const Task = require('./task.model');
const TaskService = {
async findAll(userId) { return Task.find({ user: userId }); },
async findById(id) { return Task.findById(id); },
async create(data) { return Task.create(data); },
async update(id, data) { return Task.findByIdAndUpdate(id, data, { new: true }); },
async remove(id) { return Task.findByIdAndDelete(id); },
};
module.exports = TaskService;
// features/tasks/task.controller.js
const TaskService = require('./task.service');
const asyncHandler = require('../../utils/asyncHandler');
exports.getAll = asyncHandler(async (req, res) => {
const tasks = await TaskService.findAll(req.user.id);
res.json({ success: true, data: tasks });
});
exports.create = asyncHandler(async (req, res) => {
const task = await TaskService.create({ ...req.body, user: req.user.id });
res.status(201).json({ success: true, data: task });
});
// features/tasks/task.routes.js
const express = require('express');
const controller = require('./task.controller');
const auth = require('../../middleware/authenticate');
const router = express.Router();
router.use(auth); // protect all task routes
router.get('/', controller.getAll);
router.post('/', controller.create);
module.exports = router;
// app.js
const taskRoutes = require('./features/tasks/task.routes');
app.use('/api/tasks', taskRoutes);
Common Mistakes
Mistake 1 — Mixing module.exports and exports
❌ Wrong — module.exports overwrites the exports shorthand object:
exports.add = (a, b) => a + b; // sets exports.add
module.exports = { multiply: (a, b) => a * b }; // replaces entire exports!
// exports.add is now GONE — only multiply is exported
✅ Correct — use one or the other consistently:
// Option A: module.exports object
module.exports = {
add: (a, b) => a + b,
multiply: (a, b) => a * b,
};
// Option B: exports shorthand (only for adding properties)
exports.add = (a, b) => a + b;
exports.multiply = (a, b) => a * b;
Mistake 2 — Missing .js extension in ESM imports
❌ Wrong — ESM requires explicit file extensions in Node.js:
import { handler } from './auth'; // ERR_MODULE_NOT_FOUND in Node.js ESM
import { handler } from './auth.ts'; // also wrong — TypeScript compiles to .js
✅ Correct — always include .js extension (TypeScript resolves .ts from .js):
import { handler } from './auth.js'; // correct in both Node.js ESM and TypeScript
Mistake 3 — Trying to require() an ESM package
❌ Wrong — ERR_REQUIRE_ESM thrown at runtime:
// In a CommonJS file:
const chalk = require('chalk'); // chalk v5+ is ESM-only — this crashes!
✅ Correct — use dynamic import() or choose an older CJS-compatible version:
// Option A: dynamic import (async)
const { default: chalk } = await import('chalk');
// Option B: install the last CJS version
// npm install chalk@4 (chalk@4 is CommonJS)
Quick Reference
| Task | CommonJS | ES Modules |
|---|---|---|
| Default export | module.exports = value |
export default value |
| Named export | exports.name = value |
export const name = value |
| Import default | const x = require('./m') |
import x from './m.js' |
| Import named | const { a } = require('./m') |
import { a } from './m.js' |
| Import all | const m = require('./m') |
import * as m from './m.js' |
| Dynamic import | await import('./m.js') |
await import('./m.js') |
| Current directory | __dirname |
dirname(fileURLToPath(import.meta.url)) |
| Enable ESM project | — | "type": "module" in package.json |