Chapter 4 of 18 · Node.js for Beginners — Complete Free Tutorial
The Node Module System
Anything bigger than a toy script needs to be split across files. The Node.js for Beginners series now covers the module system: how files export functionality, how other files import it, how Node finds a module from a require call, and how the newer ES module syntax fits in. Both styles appear in the wild, so you will learn to read and write each.
Why modules exist
Without modules, every file shares one global scope and load order decides everything. Modules give each file its own private scope and force relationships to be declared:
- Encapsulation — internals stay hidden; callers use only what you export.
- Reusability — one
utils.jsserves every consumer. - Explicit dependencies — imports tell you at a glance what a file needs.
- Testability — small exported functions are easy to test in isolation.
CommonJS: module.exports and require
CommonJS is Node's original format. The exporting file attaches values to module.exports; the importing file receives exactly that value.
// math.js
function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }
module.exports = { add, subtract }; // the object consumers will get
// app.js
const { add, subtract } = require('./math');
console.log(add(7, 5)); // → 12
console.log(subtract(7, 5)); // → 2
Expected output:
12
2
- Relative paths need
./(or../) — bare names are reserved for packages and built-ins. - Omitting the extension is fine: Node tries
.js, then.json, then.node. exportsis a shortcut formodule.exports— assigning tomodule.exportsitself reassigns the whole export (a classic trap).
How Node resolves a require call
| Specifier | Example | Resolved from |
|---|---|---|
| Built-in | require('fs') | The Node runtime itself (always available) |
| Relative | require('./lib/store') | Relative to the calling file |
| Absolute | require('/opt/app/db') | That exact path |
| Package | require('express') | node_modules, walking up directory by directory |
Two properties matter early: modules are cached — the first load runs the file, later loads get the same object — and resolution happens against the file's location, not the shell's working directory. Caching makes shared state possible (a config module really is a singleton) and also explains why changing a file does not change a running process: the old copy stays in memory until you restart. The path rule explains another classic head-scratcher — require('./styles.css') works from a script in the same folder but fails from anywhere else, because "same folder" is relative to the caller.
Built-in modules you will use
const fs = require('fs');
const path = require('path');
const os = require('os');
const http = require('http');
console.log(path.join('logs', 'app.txt')); // → logs/app.txt (on macOS/Linux)
console.log(os.platform()); // → darwin
Expected output:
logs/app.txt
darwin
No install step is required for built-ins; they are part of the runtime. Newer code may import them with the node: prefix (require('node:fs')) which makes the origin explicit and guards against a future package name clash.
ES modules: import and export
ES modules (ESM) are the JavaScript standard — the same syntax you use in browsers (see JavaScript Modules). To activate them in Node, set the package type:
{
"name": "my-app",
"version": "1.0.0",
"type": "module"
}
With that flag, .js files in the project are ESM:
// math.mjs or type: module project
export const add = (a, b) => a + b;
// app.js
import { add } from './math.js';
console.log(add(7, 5)); // → 12
Key differences: imports are hoisted and asynchronous in resolution, paths must include the extension (./math.js), __dirname does not exist (use import.meta.dirname), and you cannot dynamically require() — use a dynamic import() expression instead.
Choosing CommonJS or ESM
| Situation | Prefer |
|---|---|
| Brand-new project, browser parity, modern tooling | ESM ("type": "module") |
Legacy codebase or older dependency that calls require | CommonJS |
| Need synchronous conditional loading | CommonJS (require is sync; import() is async) |
| Publishing a dual-purpose library | Ship both builds or ESM with a CommonJS wrapper |
This course shows both: CommonJS where existing examples use it, ESM where new code benefits. Never mix import and require in the same file mode — Node will throw ReferenceError: require is not defined (or the reverse). Whichever you pick, decide once per package: the choice changes how every file in that folder is parsed, so consistency across the project matters more than the particular winner.
Common mistakes
- Relative path without
./—require('math')looks innode_modulesand fails. - Reassigning
exports—exports = {...}changes a local variable; usemodule.exports = {...}. - Mixing module formats in one package — decide per package;
typecannot be toggled per file. - Forgetting the file extension in ESM —
import './math'is a syntax Node rejects. - Assuming a module re-runs each import — caching means shared state; design modules as configuration or as stateless functions, not somewhere in between.
Practice checklist
- Create
greeter.jsexporting a function, and call it frommain.jswithrequire. - Export an object of three helpers and destructure them on import.
- Log inside a required file twice and prove the caching behaviour with a single printed line.
- Convert the pair to ESM: add
"type": "module", switch toexport/import, fix the path. - Require a built-in (
node:os) and print two pieces of system information.
Modules are how code is organised locally — packages are how it is shared globally. Next: npm and package.json.
Frequently Asked Questions
Node.js is a runtime that executes JavaScript outside the browser. It combines Google's V8 engine with APIs for files, networks, processes and streams, plus a package ecosystem (npm). You use it to build servers, command-line tools, APIs and anything that needs JavaScript running on a machine rather than in a page.
JavaScript is the language; Node.js is one environment that runs it. In the browser the language comes with a DOM, events and window APIs — in Node those are replaced by the file system, network and process modules. Syntax, functions, arrays, promises and async/await are identical, so your JavaScript knowledge transfers directly.
Browser code is sandboxed: it cannot read your files or open arbitrary ports. Node code has full machine access, which is why it is used for servers and tools but also why you never run untrusted code in it. There is no document or window in Node — the equivalents are global, process and modules.
npm is JavaScript's package manager and public registry. package.json describes your project: name, entry point, dependencies with version ranges, and scripts such as npm run dev. Installing a package copies it into node_modules and records the exact intent in package-lock.json so every machine installs identical versions.
Express is a minimal web framework for Node: a few thousand lines that give you routing, middleware, request parsing and static files on top of the raw HTTP server. It is the most common starting point for REST APIs and the backbone of this tutorial's project chapters.
Both work. CommonJS (require/module.exports) is the classic Node format; ES modules (import/export) are the standard and activated in a package.json with "type": "module". Modern projects favour ESM for browser compatibility, but you will read both in existing code, so this course teaches each.
Middleware are functions that run between the incoming request and the final response, in order: body parsing, logging, authentication, rate limits, error handling. Each receives the request, response and next function and can respond itself or pass control along — Express's pipeline model in one sentence.
A JSON file is perfect for learning CRUD: no setup, readable data, easy to reset. The moment you need concurrent writes, querying or multiple users, move to a real database — MySQL/PostgreSQL for relational data or MongoDB for documents. The storage chapter isolates persistence behind one module so swapping it later is straightforward.
A REST API exposes resources over HTTP using verbs on URLs: GET /api/todos lists, POST creates, PUT or PATCH updates, DELETE removes, and everything answers with JSON plus correct status codes (200, 201, 400, 404, 500). It is stateless — each request carries its own authentication — which makes it easy to cache, scale and consume from any frontend.
Node apps are long-running processes, not static files: deploy them to a Node-friendly host (a VPS, Render, Railway or Fly.io), keep secrets in environment variables, terminate HTTPS at a reverse proxy or platform edge, and send logs somewhere durable. The final chapter covers the full checklist including health checks and graceful shutdown.