Node.js Module System: CommonJS and ES Modules Explained
이 글의 핵심
A practical guide to Node.js modules: CommonJS and ES modules, exports, caching, circular dependencies, and real patterns used in production apps.
Introduction
What is a module?
A module is a reusable unit of code in its own file. Node.js supports two systems:
- CommonJS — default in Node (
require,module.exports) - ES modules — standard ECMAScript syntax (
import,export)
Benefits:
- Reuse code across files
- Avoid polluting the global scope
- Keep features separated for maintenance
- Express dependencies explicitly
- Test units in isolation
1. CommonJS modules
Basics
Export:
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
const PI = 3.14159;
module.exports = {
add,
subtract,
PI
};
Import:
// app.js
const math = require('./math');
console.log(math.add(10, 5));
console.log(math.subtract(10, 5));
console.log(math.PI);
exports vs module.exports
// OK: add properties to exports (same object as module.exports)
exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;
// OK: replace module.exports entirely
module.exports = {
add: (a, b) => a + b,
subtract: (a, b) => a - b
};
// WRONG: reassigning exports breaks the link to module.exports
exports = {
add: (a, b) => a + b
};
Conceptually, Node wraps your file and passes module and exports where exports starts as a reference to module.exports. Reassigning exports only changes the local variable.
Rules:
exports.foo = ...— OKmodule.exports = ...— OKexports = { ... }— does not change whatrequirereturns
Export patterns
Multiple functions:
// utils.js
exports.formatDate = (date) => date.toISOString().split('T')[0];
exports.capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
Single class:
// user.js
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
greet() {
return `Hello, ${this.name}!`;
}
}
module.exports = User;
Singleton:
// database.js
class Database {
constructor() {
this.connection = null;
}
connect() {
if (!this.connection) {
this.connection = { connected: true };
}
return this.connection;
}
}
module.exports = new Database();
Factory:
// logger.js
function createLogger(prefix) {
return {
log: (message) => console.log(`[${prefix}] ${message}`),
error: (message) => console.error(`[${prefix}] ERROR: ${message}`)
};
}
module.exports = createLogger;
2. ES modules
Setup
package.json:
{
"type": "module"
}
Or use the .mjs extension.
Named exports
// math.mjs
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export const PI = 3.14159;
export { multiply, divide };
// app.mjs
import { add, subtract, PI } from './math.mjs';
import { add as plus } from './math.mjs';
import * as math from './math.mjs';
Default export
// calculator.mjs
export default class Calculator {
add(a, b) { return a + b; }
subtract(a, b) { return a - b; }
}
export const VERSION = '1.0.0';
import Calculator, { VERSION } from './calculator.mjs';
Dynamic import()
async function loadModule() {
if (condition) {
const mod = await import('./heavy-module.mjs');
mod.doSomething();
}
}
3. CommonJS vs ES modules
| Feature | CommonJS | ES modules |
|---|---|---|
| Syntax | require, module.exports | import, export |
| Loading | Synchronous require at runtime | Parsed statically; async load |
| Extension | .js (default) | .mjs or "type": "module" |
| Default export | module.exports = ... | export default |
| Tree shaking | Limited | Yes |
| Browser | No (without bundler) | Native in modern browsers |
Use CommonJS for legacy code, many older packages, or quick scripts.
Use ES modules for new apps, shared browser/Node code, and bundler-friendly tree shaking.
Interop
From CommonJS, load ESM:
async function loadESM() {
const mod = await import('./es-module.mjs');
mod.default();
}
From ESM, load CommonJS:
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const cjs = require('./commonjs-module.js');
4. Built-in modules (overview)
const fs = require('fs');
const path = require('path');
const http = require('http');
const https = require('https');
const url = require('url');
const querystring = require('querystring');
const os = require('os');
const crypto = require('crypto');
const EventEmitter = require('events');
const stream = require('stream');
const child_process = require('child_process');
fs (promises)
const fs = require('fs').promises;
const path = require('path');
async function fileOperations() {
try {
const data = await fs.readFile('input.txt', 'utf8');
await fs.writeFile('output.txt', 'Hello, Node.js!', 'utf8');
await fs.appendFile('output.txt', '\nMore', 'utf8');
await fs.copyFile('output.txt', 'backup.txt');
await fs.rename('backup.txt', 'backup-new.txt');
await fs.unlink('backup-new.txt');
const stats = await fs.stat('output.txt');
console.log(stats.size, stats.mtime);
await fs.mkdir('new-folder', { recursive: true });
const files = await fs.readdir('.');
await fs.rmdir('new-folder');
} catch (err) {
console.error('Error:', err.message);
}
}
fileOperations();
path
const path = require('path');
const filePath = path.join(__dirname, 'data', 'users.json');
const absolutePath = path.resolve('data', 'users.json');
console.log(path.basename('/foo/bar/file.txt')); // file.txt
console.log(path.basename('/foo/bar/file.txt', '.txt')); // file
console.log(path.dirname('/foo/bar/file.txt')); // /foo/bar
console.log(path.extname('file.txt')); // .txt
console.log(path.parse('/foo/bar/file.txt'));
console.log(path.normalize('/foo/bar/../baz'));
console.log(path.relative('/foo/bar', '/foo/baz/file.txt'));
os
const os = require('os');
console.log('platform:', os.platform());
console.log('CPUs:', os.cpus().length);
console.log('total mem GB:', (os.totalmem() / 1024 / 1024 / 1024).toFixed(2));
console.log('free mem GB:', (os.freemem() / 1024 / 1024 / 1024).toFixed(2));
console.log('homedir:', os.homedir());
console.log('tmpdir:', os.tmpdir());
console.log('networkInterfaces:', os.networkInterfaces());
crypto
const crypto = require('crypto');
function hashPassword(password) {
return crypto.createHash('sha256').update(password).digest('hex');
}
const randomString = crypto.randomBytes(16).toString('hex');
const { randomUUID } = require('crypto');
console.log(randomUUID());
5. Module caching
// counter.js
let count = 0;
exports.increment = () => {
count++;
console.log('Count:', count);
};
exports.getCount = () => count;
const counter1 = require('./counter');
const counter2 = require('./counter');
counter1.increment();
counter2.increment();
console.log(counter1 === counter2); // true
Clear cache (testing):
delete require.cache[require.resolve('./counter')];
const counter3 = require('./counter');
6. Circular dependencies
Problem: a.js requires b.js, b.js requires a.js — partially initialized exports may be undefined.
Fix 1 — shared module:
// shared.js
exports.nameA = 'Module A';
exports.nameB = 'Module B';
Fix 2 — lazy require inside a function:
exports.greet = () => {
const a = require('./a');
console.log(`B sees: ${a.name}`);
};
Fix 3 — dependency injection (pass dependencies after both load).
7. Module resolution
require('./math'); // relative
require('../utils/math');
require('express'); // node_modules
require('fs'); // built-in
Resolution order for ./math: math.js, math.json, math.node, math/index.js, or math/package.json main.
require.resolve('express') shows the resolved path.
8. Practical examples
Common patterns include config (dotenv, structured module.exports), logger (singleton writing to disk), database wrapper singleton, and ApiClient with https.request. Translate user-facing strings for your locale in your own codebase.
9. package.json advanced
{
"name": "my-package",
"version": "1.0.0",
"main": "index.js",
"dependencies": { "express": "^4.18.2" },
"devDependencies": { "nodemon": "^3.0.1" }
}
Semantic versioning: ^4.18.2 allows minor/patch updates below 5.0.0; ~4.18.2 allows patch only; pin exact versions when you need reproducible builds.
Lifecycle npm scripts: prebuild, build, postbuild run in order when you npm run build.
10. Module patterns
Singleton with guard:
class Database {
constructor() {
if (Database.instance) return Database.instance;
this.connection = null;
Database.instance = this;
}
}
module.exports = new Database();
IIFE module with private state:
const counter = (() => {
let count = 0;
return {
increment() { return ++count; },
getCount() { return count; }
};
})();
module.exports = counter;
11. Common problems
Cannot find module
Check path typos, install the package, or add "type": "module" / .mjs for ESM.
import outside a module
Add "type": "module" to package.json or rename to .mjs.
No __dirname in ESM
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
12. Project structure tip
src/
├── config/
├── models/
├── controllers/
├── routes/
├── middlewares/
├── utils/
└── index.js
Use models/index.js to re-export models for cleaner imports.
Summary
- CommonJS —
require/module.exports - ES modules —
import/export - Caching — one evaluation per resolved path
- Circular deps — refactor, lazy load, or inject
- package.json — metadata, semver, scripts
Next steps
Resources
Related posts
- JavaScript modules (ESM vs CJS)
- Getting started with Node.js
- Async: callbacks, Promises, async/await