Course outline

Node.js / Basic: the Node.js runtime / Lesson 2

Modules, packages, and npm

Build explicit module boundaries and understand package metadata and repeatable dependency installation before adding third-party code.

About 4 min readContent reviewed
On this page

Lesson goals

  • Explain the concepts and trade-offs behind modules, packages, and npm.
  • Predict and verify both examples, including their outputs and cleanup behavior.
  • Diagnose the three common failures and apply the worked solutions to related inputs.

Separate responsibilities

A module groups related behavior behind exports. Its consumers depend on those exports rather than every local variable. Split a program where responsibilities change: parsing input, applying a rule, and writing output are useful boundaries. Avoid creating dozens of tiny files without a purpose, and avoid a single file that owns every concern. Modules are evaluated when loaded and normally cached by resolved identity, so top-level side effects can become surprising shared state. Prefer exporting functions that receive configuration explicitly. This makes imports safe in tests and prevents importing a helper from unexpectedly starting a server.

Make module format explicit

This course uses .mjs files, which explicitly select ECMAScript modules. Use import and export consistently. Relative imports include the file extension, and the node: prefix identifies built-in modules. CommonJS uses .cjs and require; package.json can also declare a type for .js files. Interoperability exists, but a beginner should not mix formats accidentally. Named imports must match named exports, while a default export has its own import form. An import path is resolved relative to the importing module, unlike many filesystem paths that are resolved relative to the working directory.

Observe a real export

The first standalone example exports a function and calls it from the same file. Exporting does not change arithmetic or automatically expose an HTTP endpoint. The second example writes a tiny module into a temporary directory, imports it through a file URL, and removes the directory in finally. This demonstrates an actual module boundary without requiring an undisplayed companion file. pathToFileURL handles characters that would otherwise have URL meaning. Production source modules normally live in the repository; temporary generation is only a teaching technique used to keep this demonstration self-contained.

javascript Complete example: Observe a real export
export function subtotal(price, count) { return price * count; }
console.log(subtotal(7, 3));
console.log(import.meta.url.startsWith('file:'));
Expected output
21
true

The named export remains an ordinary function. import.meta.url identifies this module as a file URL.

Understand npm before installation

A package.json records package metadata, scripts, and dependency ranges. A lockfile records the resolved dependency graph needed for repeatable installation. npm install intentionally updates resolution when appropriate; npm ci expects a compatible lockfile and performs a clean installation suited to automation. Neither command proves that code is trustworthy. A dependency can execute lifecycle scripts and bring transitive packages, so review necessity, maintenance, licenses, and security advisories. These lessons need no external dependencies. Built-in modules are already part of Node and must not be installed from similarly named registry packages.

javascript Complete example: Understand npm before installation
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
const dir = await mkdtemp(join(tmpdir(), 'module-'));
try {
  const file = join(dir, 'math.mjs');
  await writeFile(file, 'export const double = n => n * 2;');
  const { double } = await import(pathToFileURL(file).href);
  console.log(double(9));
} finally { await rm(dir, { recursive: true, force: true }); }
Expected output
18

A dynamically imported named export doubles nine. Cleanup runs even if module evaluation fails.

Design a reproducible project

Commit the manifest and lockfile together, document the runtime, and expose a small set of meaningful scripts such as test and start. Keep development tools distinct from dependencies needed by a deployed application. Review lockfile changes as code changes because a small manifest edit can replace many packages. When an import fails, inspect the exact resolved path and package exports before reinstalling everything. A reliable module design also avoids circular initialization dependencies: two files that require each otherโ€™s not-yet-initialized values usually indicate a responsibility that should be extracted into a third module.

Common errors and fixes

ReferenceError: require is not defined

Why it happens
CommonJS syntax was placed in an ESM file.
How to fix it
Use import or deliberately isolate CommonJS in .cjs.
Prevent it next time
Choose one explicit module format for application source.

ERR_MODULE_NOT_FOUND

Why it happens
A relative path omitted its extension or points at another directory.
How to fix it
Correct the path relative to the importing file.
Prevent it next time
Test imports on case-sensitive CI filesystems.

npm ci rejects the lockfile

Why it happens
Manifest and lockfile describe different dependency states.
How to fix it
Regenerate intentionally with npm install and review both changes.
Prevent it next time
Commit both files in the same change.

Check your understanding

1. Choose the right import for export const tax = 2 in tax.mjs.

Show worked solution

Use import { tax } from "./tax.mjs". Braces select the named export, the relative prefix selects a local file, and the extension removes ambiguity. Printing tax then yields 2. A default import would request a different export contract.

2. A team wants identical dependencies in CI without updating versions. What should it commit and run?

Show worked solution

Commit package.json and package-lock.json, select the documented Node/npm runtime, then run npm ci. If it fails on a mismatch, repair the files through a reviewed development change rather than silently regenerating the lock inside CI.

Key takeaways

  • A module groups related behavior behind exports.
  • This course uses .mjs files, which explicitly select ECMAScript modules.
  • Commit the manifest and lockfile together, document the runtime, and expose a small set of meaningful scripts such as test and start.

Official references