A module in Node.js is a file or package that contains reusable code. Instead of writing every function in one large file, modules let you break an application into smaller pieces with clear responsibilities. For example, one module might handle database logic, another might format dates, another might define routes, and another might validate user input. This makes the code easier to read, reuse, test, and maintain.
Each module has its own scope. Variables and functions declared inside one module are not automatically available in another file unless you explicitly export them. That isolation is very helpful because it prevents accidental naming conflicts and reduces the chance that unrelated parts of the program will interfere with one another.
Node.js projects usually work with three broad categories of modules:
Built-in modules come with Node.js, so you do not need to install them separately. These modules provide many of the essential features that make Node.js useful as a server-side runtime, including HTTP handling, file access, path operations, streams, events, and system information.
A built-in module is loaded with require() in CommonJS or import in ES modules. Since the module already ships with Node.js, there is no need to run npm install first.
| Module | Description |
|---|---|
| http | Create HTTP servers and handle requests and responses. |
| fs | Work with files and directories. |
| path | Handle and transform file paths safely. |
| url | Parse and construct URLs. |
| events | Create and use event-based communication. |
| os | Get operating system information. |
| util | Utility helpers used in Node.js programs. |
const path = require("path");
const fullPath = "C:/projects/node/app.js";
console.log(path.basename(fullPath)); // app.js
console.log(path.dirname(fullPath)); // C:/projects/node
console.log(path.extname(fullPath)); // .js
Local modules are files created by you inside the project. They are one of the main tools for structuring real applications. For example, in an API project, you might keep route handlers in one folder, database models in another, and utility functions in another. Instead of writing everything inside index.js or server.js, you split the logic into multiple files and import only what you need.
When you load a local module in CommonJS, you use a relative path such as ./math.js or ../utils/logger.js. The ./ means "from the current folder." This is different from built-in modules and installed packages, which are referenced by name only.
This example shows one of the most common export patterns: exporting an object that contains several functions. It is simple, readable, and useful when a module needs to provide multiple related utilities.
// math.js
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
module.exports = {
add,
multiply
};
// app.js
const math = require("./math");
console.log(math.add(5, 3)); // 8
console.log(math.multiply(4, 6)); // 24
Third-party modules are external packages installed from npm. They allow you to reuse code written by the wider community instead of implementing every feature yourself. For example, Express helps you create web servers more easily, Axios helps you call APIs, and Dotenv helps you load environment variables from a file.
Third-party packages are stored in node_modules and tracked through package.json and package-lock.json. This makes it easy for other developers to install the same dependencies by running npm install.
npm install lodash
const _ = require("lodash");
const numbers = [1, 2, 3, 4, 5];
const reversed = _.reverse([...numbers]);
console.log(reversed); // [5, 4, 3, 2, 1]
Historically, Node.js used the CommonJS module system by default. In CommonJS, code is imported with require() and exported with module.exports or exports. Many existing Node.js tutorials, packages, and codebases still use this format, so it remains very important to understand.
A useful guideline is this: use module.exports when you want to export one main value, and use exports.someName when you want to expose several named members. Many developers prefer module.exports = { ... } even for multiple exports because it keeps the structure explicit in one place.
// Export one function directly
module.exports = function greet(name) {
return `Hello, ${name}`;
};
// Export multiple named items
exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;
Modern Node.js also supports ES Modules, which use import and export syntax. This is the standard JavaScript module format used in browsers as well. ES Modules can be enabled by using the .mjs file extension or by setting "type": "module" inside package.json.
If your project uses ES Modules, keep the syntax consistent. Mixing CommonJS and ESM without understanding the differences can create confusing import behavior. Many modern projects use ESM, but CommonJS is still extremely common in the Node.js ecosystem.
// math.mjs
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
// app.mjs
import { add, multiply } from "./math.mjs";
console.log(add(2, 7)); // 9
console.log(multiply(3, 4)); // 12
The require() function does more than just "open a file." It follows a resolution process. If you pass a built-in module name like fs, Node.js loads the built-in module. If you pass a relative path like ./utils/helper, Node.js searches your project folders for that file. If you pass a package name like express, Node.js looks in node_modules.
This behavior explains why the syntax differs by module type. A relative path means "use my own file," while a bare package name means "load a built-in or installed package." Understanding this saves a lot of time when debugging "Cannot find module" errors.
Node.js caches modules after the first time they are loaded. That means if the same module is required again elsewhere, Node.js usually returns the already loaded instance instead of reading and executing the file from scratch. This improves performance and also means module-level state can be shared.
This cached behavior is useful, but it also means you should be careful with mutable shared state inside modules. If a module stores changing values at the top level, that state may be reused across different parts of the application.
// counter.js
let count = 0;
module.exports = function increment() {
count++;
return count;
};
// app.js
const increment = require("./counter");
console.log(increment()); // 1
console.log(increment()); // 2
console.log(increment()); // 3
Modules become much more meaningful when you see how they shape a real project. A beginner Node.js application might be organized like this:
In this structure, server.js may start the application, routes/users.js may define user routes, services/userService.js may contain business logic, and utils/logger.js may provide reusable logging functions. This separation is one of the main reasons modules are so valuable.
my-app/
package.json
server.js
routes/
users.js
services/
userService.js
utils/
logger.js
A common mistake is forgetting the relative path prefix when importing a local file. Writing require("math") tries to load a package or built-in module, while require("./math") loads your own file. Another mistake is mixing exports and module.exports carelessly. Since exports is just a reference, reassigning it directly can break expected exports. Beginners also sometimes assume modules are re-executed from scratch every time, forgetting that CommonJS modules are cached.
It is also easy to create modules that are too large. If one file handles routing, validation, database logic, and utility formatting together, the code becomes harder to test and maintain. Smaller modules with one clear purpose are usually easier to work with.
Think of a module as a tool drawer. Each drawer contains a specific group of tools for a specific job. If you need math helpers, open the math drawer. If you need logging helpers, open the logger drawer. If everything is thrown into one giant box, finding and reusing the right piece becomes difficult. Modules give a Node.js project a structure that scales as the application grows.
Explore 500+ free tutorials across 20+ languages and frameworks.