본문으로 건너뛰기
Previous
Next
Node.js Module System: CommonJS and ES Modules Explained

Node.js Module System: CommonJS and ES Modules Explained

Node.js Module System: CommonJS and ES Modules Explained

이 글의 핵심

Master Node.js modules: require vs import, module.exports vs export, resolution, caching, circular dependencies, package.json, and interoperability—essential for any Node.js tutorial or backend project.

Introduction

What is a module?

A module is a reusable unit of code in its own file. Node.js supports two systems:

  1. CommonJS — default in Node (require, module.exports)
  2. 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 = ... — OK
  • module.exports = ... — OK
  • exports = { ....} — does not change what require returns

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

FeatureCommonJSES modules
Syntaxrequire, module.exportsimport, export
LoadingSynchronous require at runtimeParsed statically; async load
Extension.js (default).mjs or "type": "module"
Default exportmodule.exports = ...export default
Tree shakingLimitedYes
BrowserNo (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)

The following example demonstrates the concept in javascript:

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

The following example demonstrates the concept in javascript:

// 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

Configuration file:

{
  "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

  1. CommonJSrequire / module.exports
  2. ES modulesimport / export
  3. Caching — one evaluation per resolved path
  4. Circular deps — refactor, lazy load, or inject
  5. package.json — metadata, semver, scripts

Next steps

  • [Async programming in Node.js](/en/blog/nodejs-series-03-async/
  • [Express.js guide](/en/blog/nodejs-series-04-express/
  • [File system (fs)](/en/blog/nodejs-series-05-filesystem/

Resources


  • JavaScript modules (ESM vs CJS)
  • [Getting started with Node.js](/en/blog/nodejs-series-01-intro/
  • [Async: callbacks, Promises, async/await](/en/blog/nodejs-series-03-async/

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Master Node.js modules: require vs import, module.exports vs export, resolution, caching, circular dependencies, package… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

Node.js, JavaScript, Modules, CommonJS, ES Modules, require, import, npm 등으로 검색하시면 이 글이 도움이 됩니다.