Skip to main content

📦 Core Modules and NPM Ecosystem

Node.js ships with a batteries-included standard library and sits on top of npm — the largest software registry on Earth. In this lesson you'll learn how modules work (both CommonJS and modern ES Modules), tour the core modules you'll use every day, and get comfortable with package.json, versioning, and installing dependencies.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain the three kinds of Node.js modules: core, local, and third-party
  • Compare CommonJS (require) with ES Modules (import) and choose between them
  • Use essential core modules — fs, path, os, events, and util
  • Read and write a package.json and understand semantic versioning
  • Install, audit, and manage dependencies with npm (and know its alternatives)

Estimated Time: 35–45 minutes  •  Difficulty: Beginner–Intermediate

Hands-on: Build, test, and version your own small utility module.

In This Lesson

The Module System

A module is a self-contained file of related code with its own private scope. Modules let you split a large program into small, testable pieces and reuse them across projects. Node.js recognizes three sources of modules:

  • Core modules — built into Node.js (e.g. fs, http, path). No installation needed.
  • Local modules — files you write and import by path (e.g. ./math.js).
  • Third-party modules — packages you install from npm (e.g. express).
🧱 The LEGO analogy: Core modules are the standard bricks in the box. Local modules are the custom pieces you build for this specific model. Third-party modules are the specialty kits you buy to add wings, wheels, or lights without designing them yourself.
Three sources of modules A Node.js application draws from core modules, local modules, and third-party npm packages. Your App Core modules fs · http · path · os Local modules ./math.js · ./db.js npm packages express · zod
Figure 1 — Every Node.js app pulls from three kinds of modules. You import them the same way; only the source differs.

CommonJS vs ES Modules

Node.js supports two module formats. Knowing both matters because you'll meet both in real codebases.

CommonJS (the original)

CommonJS uses require() to import and module.exports to expose. Modules load synchronously and are cached after the first load.

// math.js — a CommonJS module
function add(a, b) {
  return a + b;
}
function subtract(a, b) {
  return a - b;
}

module.exports = { add, subtract };
// app.js — importing it
const math = require('./math');

console.log(math.add(5, 3));       // 8
console.log(math.subtract(10, 4)); // 6

ES Modules (the modern standard)

ES Modules (ESM) use import/export — the same syntax you use in the browser. Enable ESM by setting "type": "module" in package.json, or by using the .mjs file extension.

// math.js — an ES module
export function add(a, b) {
  return a + b;
}
export function subtract(a, b) {
  return a - b;
}
// app.js — importing it
import { add, subtract } from './math.js';

console.log(add(5, 3));       // 8
console.log(subtract(10, 4)); // 6

✅ Which should you use?

For new projects, prefer ES Modules — they're the JavaScript standard, work in the browser and Node.js alike, and support features like top-level await. Reach for CommonJS mainly when maintaining older code or a dependency that hasn't migrated.

⚠️ Don't mix them carelessly

In ESM you must include the file extension ('./math.js', not './math'), and require, __dirname, and __filename aren't defined. Use import.meta.url plus node:url/node:path when you need the current file's path in ESM.

Essential Core Modules

These come with Node.js — no install required. Import them with the node: prefix to make their origin unmistakable.

fs — File System

Reads and writes files. Prefer the promise-based API (fs/promises) with async/await over old callback style.

import { readFile, writeFile } from 'node:fs/promises';

async function run() {
  try {
    const data = await readFile('example.txt', 'utf8');
    console.log('Content:', data);
    await writeFile('copy.txt', data);
  } catch (err) {
    console.error('File error:', err.message);
  }
}
run();

path — Cross-Platform File Paths

Builds and parses file paths correctly on Windows, macOS, and Linux — never hand-concatenate paths with slashes.

import path from 'node:path';

const full = path.join('public', 'images', 'logo.png');
console.log(full);                 // public/images/logo.png (or \ on Windows)
console.log(path.extname('report.pdf')); // .pdf
console.log(path.basename('/home/user/report.txt')); // report.txt

os — Operating System Info

import os from 'node:os';

console.log(`Platform: ${os.platform()}, Arch: ${os.arch()}`);
console.log(`Free memory: ${(os.freemem() / 1024 ** 3).toFixed(2)} GB`);
console.log(`CPU cores: ${os.cpus().length}`);

events — The EventEmitter

The EventEmitter class underpins Node.js's event-driven design. You emit named events and register listeners for them.

import { EventEmitter } from 'node:events';

const bus = new EventEmitter();

bus.on('order', (item) => {
  console.log(`New order: ${item}`);
});

bus.emit('order', 'espresso'); // New order: espresso

util — Handy Utilities

The util module offers helpers like promisify (to modernize callback APIs) and inspect (for readable object logging).

import util from 'node:util';

const obj = { user: { name: 'Ada', roles: ['admin'] } };
console.log(util.inspect(obj, { depth: null, colors: true }));

📖 Quick reference

fs — files  •  path — file paths  •  os — system info  •  events — pub/sub  •  util — helpers  •  http — servers & clients  •  crypto — hashing/encryption  •  stream — chunked data.

npm: The Package Manager

npm (Node Package Manager) is the world's largest software registry — well over two million packages — and it comes bundled with Node.js. Instead of reinventing common solutions, you install trusted, tested packages.

flowchart TD A[npm ecosystem] --> B[Public registry
npmjs.com] A --> C[Command-line tool
npm install / run] A --> D[package.json
project manifest] D --> D1[dependencies] D --> D2[scripts] D --> D3[metadata]

package.json — the project manifest

This file records your project's metadata, scripts, and dependencies. Create it with npm init -y.

{
  "name": "my-awesome-project",
  "version": "1.0.0",
  "description": "A demo project",
  "type": "module",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "dev": "node --watch index.js",
    "test": "node --test"
  },
  "dependencies": {
    "express": "^4.19.2"
  },
  "devDependencies": {
    "eslint": "^9.0.0"
  }
}

Installing packages

# Add a runtime dependency
npm install express

# Add a development-only dependency
npm install --save-dev eslint

# Install a specific version
npm install lodash@4.17.21

# Install a global command-line tool
npm install -g http-server

# Reproducible, CI-friendly install from the lockfile
npm ci

📖 Two files, two jobs

package.json lists dependencies with version ranges — like a recipe's ingredient list. package-lock.json pins the exact versions of every package (and its sub-packages) so every install is reproducible — like the precise shopping list of brands and quantities.

Semantic Versioning

npm packages use SemVer: MAJOR.MINOR.PATCH. Each number signals the kind of change:

PartBumped when…Example
MAJORBreaking, incompatible API changes4.x → 5.0.0
MINORNew features, backwards-compatible4.1 → 4.2.0
PATCHBackwards-compatible bug fixes4.2.0 → 4.2.1

The prefix in front of a version in package.json controls how far npm may upgrade:

  • ^4.17.3 — allow any 4.x.x (minor + patch updates). The common default.
  • ~4.17.3 — allow any 4.17.x (patch updates only).
  • 4.17.3 — exactly this version, no automatic updates.
  • * — any version. Avoid this in production.

💡 Why the caret matters

^ is a promise from the package author: "minor and patch releases won't break you." That promise only holds when authors follow SemVer honestly — which is why the lockfile exists as your safety net.

Best Practices & Alternatives

Security & maintenance

  • Run npm audit regularly and npm audit fix to patch known vulnerabilities.
  • Commit package-lock.json so teammates and CI install identical trees.
  • Be wary of packages with huge or unfamiliar dependency chains — each is code you're trusting.

Use npm scripts as your task runner

"scripts": {
  "start": "node server.js",
  "dev": "node --watch server.js",
  "test": "node --test",
  "lint": "eslint .",
  "format": "prettier --write ."
}

Run any of these with npm run <name> (or just npm start / npm test for those two).

Alternative package managers

ToolNotable strength
npmBundled with Node.js; the default everyone has
YarnFast, deterministic; popular for monorepos (Yarn Workspaces)
pnpmSaves disk space via a shared store; strict about phantom dependencies

⚠️ Pick one lockfile per project

Don't commit both a package-lock.json and a yarn.lock. Mixing managers leads to inconsistent installs. Choose one tool per repository and stick with it.

Hands-on Exercise

🏋️ Build a String-Utils Module

Objective: Create, export, and test your own local module.

Instructions:

  1. Make a folder: mkdir string-utils && cd string-utils.
  2. Run npm init -y, then add "type": "module" to the generated package.json.
  3. Create index.js exporting three functions: reverse, capitalize (title-case each word), and truncate(str, n) (cut to n chars with an ellipsis).
  4. Create test.js that imports and exercises each function, using Node's built-in assert.
  5. Add a "test": "node test.js" script and run it with npm test.
💡 Hint

Title-casing a sentence: str.split(' ').map(w => w[0].toUpperCase() + w.slice(1)).join(' '). For truncation, return the string unchanged when it's already short enough.

✅ Example solution
// index.js
export function reverse(str) {
  return [...str].reverse().join('');
}

export function capitalize(str) {
  return str
    .split(' ')
    .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
    .join(' ');
}

export function truncate(str, n) {
  return str.length <= n ? str : str.slice(0, n) + '…';
}
// test.js
import assert from 'node:assert';
import { reverse, capitalize, truncate } from './index.js';

assert.equal(reverse('hello'), 'olleh');
assert.equal(capitalize('hello world'), 'Hello World');
assert.equal(truncate('abcdef', 3), 'abc…');
assert.equal(truncate('hi', 5), 'hi');

console.log('All tests passed ✅');

🎯 Quick Quiz

Question 1: Which syntax belongs to ES Modules (not CommonJS)?

Question 2: In the version range ^4.17.3, which update would npm not take automatically?

Question 3: What is the main job of package-lock.json?

Summary & Quiz

🎉 Key Takeaways

  • Node.js modules come in three flavors: core, local, and third-party.
  • CommonJS (require) is the legacy format; ES Modules (import) are the modern standard — prefer them for new code.
  • Core modules like fs, path, os, events, and util cover most everyday needs with zero install.
  • package.json is your project manifest; package-lock.json guarantees reproducible installs.
  • SemVer (MAJOR.MINOR.PATCH) and range prefixes (^, ~) control how dependencies update.

📚 Further Reading

🚀 What's Next?

You've seen that Node.js APIs are heavily asynchronous. Next we'll go deep on the event loop — how Node.js schedules all that async work, the phases it cycles through, and how to write async code with callbacks, Promises, and async/await without blocking.

🎉 Great progress!

You can now assemble apps from modules and packages. Time to understand what runs them.