Difficulty: Beginner
Node.js was built with a module system called CommonJS (CJS) that allows you to split your code into separate files and share functionality between them. Every file in Node.js is treated as a separate module with its own scope. Variables, functions, and classes defined in one file are private to that file unless explicitly exported. This encapsulation prevents naming collisions and encourages modular, maintainable code.
To export values from a module, you use module.exports or the exports shorthand. module.exports is the actual object that gets returned when another file requires your module. By default, it is an empty object {}. You can assign a single value (a function, class, or object) directly to module.exports, or you can add properties to it. The exports variable is a reference to module.exports, so adding properties to exports (like exports.myFunc = ...) also adds them to module.exports. However, reassigning exports directly (exports = something) breaks the reference and does not work.
To import values from another module, you use the require() function. When you call require('./myModule'), Node.js loads the file, executes it, and returns whatever module.exports was set to. The path can be a relative path (starting with ./ or ../), an absolute path, or a module name (for built-in modules like 'fs' or npm packages). For relative paths, Node.js resolves .js, .json, and .node extensions automatically, and if the path points to a directory, it looks for an index.js file inside.
Node.js caches modules after the first require() call. If you require the same module from multiple files, the module code runs only once and the same exported object is returned each time. This means if a module exports an object and one file modifies that object, the change is visible to all other files that require the same module. Module caching is based on the resolved filename, so requiring './utils' and requiring '/home/user/project/utils' for the same file results in a cache hit.
The module resolution algorithm follows a specific order: (1) core modules like fs, path, http always take priority, (2) relative paths (./file or ../file) resolve relative to the current file, (3) for bare module names like 'express', Node.js walks up the directory tree checking each node_modules folder until it finds the package or reaches the root. Understanding this algorithm helps you debug situations where the wrong version of a module is loaded.
// --- math.js ---
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
// Export multiple functions
module.exports = { add, subtract };
// --- app.js ---
const { add, subtract } = require('./math');
console.log(add(10, 5));
console.log(subtract(10, 5));
// You can also import the whole object
const math = require('./math');
console.log(math.add(3, 4));
module.exports is set to an object containing both functions. When requiring, you can destructure to get individual functions or import the entire object. The .js extension is optional in require paths.
// --- logger.js ---
// Using exports shorthand (adds to module.exports)
exports.info = (msg) => console.log(`[INFO] ${msg}`);
exports.warn = (msg) => console.log(`[WARN] ${msg}`);
exports.error = (msg) => console.log(`[ERROR] ${msg}`);
// --- counter.js ---
// Exporting a single value (must use module.exports)
let count = 0;
module.exports = {
increment() { return ++count; },
decrement() { return --count; },
getCount() { return count; }
};
// --- app.js ---
const logger = require('./logger');
const counter = require('./counter');
logger.info('Starting app');
console.log(counter.increment());
console.log(counter.increment());
console.log(counter.getCount());
logger.warn('Counter is at ' + counter.getCount());
The exports shorthand works for adding named exports. For exporting a single constructor, class, or specific object, use module.exports directly. The counter module demonstrates that the private 'count' variable maintains state across requires due to module caching.
// --- config.js ---
console.log('Config module loaded');
module.exports = {
dbHost: 'localhost',
dbPort: 5432
};
// --- app.js ---
// First require: executes config.js and caches result
const config1 = require('./config');
console.log('First require:', config1.dbHost);
// Second require: returns cached module (no re-execution)
const config2 = require('./config');
console.log('Second require:', config2.dbHost);
// Both references point to the same object
config1.dbHost = 'production-server';
console.log('config2 sees change:', config2.dbHost);
console.log('Same object?', config1 === config2);
'Config module loaded' prints only once because Node.js caches the module after the first require. Both config1 and config2 reference the exact same object. Modifying one affects the other. This is why singleton patterns work naturally with CommonJS modules.
require, module.exports, exports, Module Resolution, Module Caching