Node.js Essentials: Modules and npm
Modules are the building blocks of any Node.js application. They let you organize code into reusable pieces, share functionality across files, and tap into thousands of packages from the npm ecosystem. In this tutorial, you will learn how to create modules, import them into other files, and manage dependencies with npm.
Understanding Modules in Node.js
Every JavaScript file in Node.js is treated as a module. When you create a file called greeting.js, it becomes a module that can be imported into other files. Node.js uses the CommonJS module system by default, which provides require() for importing and module.exports for exporting.
Creating Your First Module
Create a new file called greeting.js and add a simple function:
// greeting.js
function sayHello(name) {
return `Hello, ${name}!`;
}
module.exports = sayHello;
The module.exports statement makes whatever you assign available to other files when they import this module. You can export functions, objects, classes, or any JavaScript value.
Importing a Module
Now create an index.js file in the same directory and import your greeting function:
// index.js
const sayHello = require('./greeting');
console.log(sayHello('World'));
// Output: Hello, World!
When you run node index.js, Node.js loads greeting.js, executes it, and returns whatever was assigned to module.exports. The ./ prefix tells Node.js to look in the current directory.
Exporting Multiple Values
Sometimes you want to export multiple functions or values from a single module. There are two common patterns for this.
Pattern 1: Export an Object
// math.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
const multiply = (a, b) => a * b;
module.exports = {
add,
subtract,
multiply
};
// app.js
const math = require('./math');
console.log(math.add(5, 3)); // Output: 8
console.log(math.multiply(4, 2)); // Output: 8
Pattern 2: Destructuring on Import
You can also destructure directly when importing:
const { add, multiply } = require('./math');
console.log(add(10, 5)); // Output: 15
console.log(multiply(3, 7)); // Output: 21
Both patterns work well. The choice depends on your preference and how many exports you need.
Introduction to npm
npm (Node Package Manager) is the default package manager for Node.js. It comes bundled with Node.js and gives you access to a massive registry of open-source packages. npm also helps you manage your project’s dependencies.
Initializing a New Project
Run npm init in your project directory to create a package.json file:
npm init
This interactive command asks several questions about your project, including the name, version, description, entry point, test command, git repository, keywords, author, and license. You can also use npm init -y to accept all defaults and skip the interactive prompts. This creates a basic package.json file with default values that you can edit later as needed.
{
"name": "my-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
The package.json file tracks your project’s metadata and dependencies. Understanding its structure is essential for Node.js development. The main fields include “name” for your project identifier, “version” following semantic versioning (major.minor.patch), “main” pointing to your entry file, and “scripts” defining automation commands. The “dependencies” section lists packages required at runtime, while “devDependencies” lists packages only needed during development.
Installing Packages
Install a package using npm install package-name. For example, to add the popular lodash library:
npm install lodash
This does two things:
- Downloads the package into a node_modules folder
- Adds the dependency to package.json
{
"dependencies": {
"lodash": "^4.17.21"
}
}
Using Installed Packages
Once installed, you can import packages just like local modules:
const _ = require('lodash');
const numbers = [1, 2, 3, 4, 5];
console.log(_.sum(numbers)); // Output: 15
console.log(_.shuffle(numbers)); // Output: [3, 1, 5, 2, 4] — random order
Development Dependencies
Packages you only need during development (like testing frameworks) should be installed as dev dependencies:
npm install --save-dev jest
These appear in a separate section of package.json:
{
"dependencies": {
"lodash": "^4.17.21"
},
"devDependencies": {
"jest": "^29.0.0"
}
}
Understanding node_modules
The node_modules folder contains all packages your project depends on, including their own dependencies. This folder can grow quite large as you install more packages, but you should never commit it to version control. Including node_modules in your repository would bloat the repository size significantly and cause unnecessary conflicts between team members who may have different operating systems or package versions.
Add node_modules to your .gitignore file:
echo "node_modules/" >> .gitignore
Or create a .gitignore file with these contents:
node_modules/
When you clone a repository, run npm install (or npm ci for CI/CD environments) to reinstall all dependencies from the package.json lock file. The lock file ensures everyone on your team gets the exact same versions of all packages, preventing the “it works on my machine” problem.
Summary
You have learned how to create and import modules in Node.js using require() and module.exports. You have also seen how npm helps you manage project dependencies, install packages, and organize your code. These fundamentals are essential for building any Node.js application.
In the next tutorial of this series, you will learn how to work with the file system in Node.js.
Keep package.json Trustworthy
package.json is the project contract. It tells npm what the app needs, which scripts exist, and how the package should be identified. Keep it tidy and review changes carefully, because a small edit there can change install behavior across the whole team. That file is often the first place to look when dependency versions or scripts feel off.
Know When to Use Local Files
A local module is the right choice when code only belongs to one project. A package dependency is the right choice when you want versioned behavior from outside the repo. That split helps you avoid copying utility code from file to file and makes it easier to replace a package later if the project needs a different path.
Keep Your Workflow Small
The simplest npm workflow is often the best one: initialize, install, run, and update. Learn the few commands you use every week, then keep the rest of the process visible in package scripts. That reduces the amount of shell ceremony you need to remember and makes new teammates faster when they open the project.
Review Lockfiles Together
When dependencies change, the lockfile should travel with the package manifest. Reviewing them together makes it easier to see whether the update is expected or accidental. That habit also helps you catch package churn before it becomes a larger maintenance issue.
Keep Script Names Stable
Scripts become part of the team’s muscle memory. If test, build, and start always mean the same thing, it is much easier to hand the project to someone new. Stable names turn the project into a set of predictable commands instead of a guessing game.
Keep Dependencies Visible
Dependency changes should be easy to review because they affect install size, startup behavior, and the package tree. Review lockfile and package updates together so you can see what changed and why. That habit makes upgrades much less surprising.
Scripts Turn Commands Into Policy
Package scripts give the team a shared entry point for linting, testing, and running the app. That is better than asking everyone to remember a long shell command. When the script names stay stable, the project is easier to hand off.