Chapter 14: Node Modules
What is a Module in Node.js?
A module is simply a file (or sometimes a folder) that contains JavaScript code you want to reuse in other parts of your application.
Most common things we put inside modules:
- Utility functions (formatters, validators, helpers)
- Classes / Models
- Route handlers / controllers
- Service layer (business logic)
- Database helpers / repositories
- Configuration values
- Middleware
- Custom error classes
Main goals of using modules:
- Keep files small and focused (single responsibility)
- Avoid huge 2000+ line files
- Make code easier to test
- Make code easier to reuse
- Make team collaboration easier
Chapter 2 – Two Module Systems in Node.js (very important to understand)
| System | Syntax used | Introduced in | Status in 2026 | Recommended for new projects? | File extension requirement |
|---|---|---|---|---|---|
| CommonJS | require() / module.exports | 2009 | Still very widely used | No – legacy | No (.js is optional) |
| ES Modules (ESM) | import / export | Node.js 12+ | Modern standard | Yes | Usually yes (.js) |
2026 reality check:
- New libraries are mostly ESM-only or dual
- Most new projects start with ESM
- Many enterprise / legacy projects still use CommonJS
- You need to know both — especially when you work with older codebases
Chapter 3 – CommonJS – The Classic Way (still very common)
3.1 Exporting in CommonJS
utils/math.js
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// Single value export const PI = 3.14159265359; // Multiple named exports function add(a, b) { return a + b; } function subtract(a, b) { return a - b; } // Default-style export (most common pattern) const mathOperations = { add, subtract, PI }; // This is how you export in CommonJS module.exports = mathOperations; // You can also do: // module.exports.add = add; // module.exports.PI = PI; // But the object pattern is cleaner |
3.2 Importing in CommonJS
index.js
|
0 1 2 3 4 5 6 7 8 9 10 |
const math = require('./utils/math'); console.log("PI:", math.PI); console.log("5 + 3 =", math.add(5, 3)); console.log("10 - 4 =", math.subtract(10, 4)); |
Destructuring is also very common:
|
0 1 2 3 4 5 6 7 8 |
const { add, subtract, PI } = require('./utils/math'); console.log(add(7, 8)); // 15 |
Important notes about CommonJS:
- No need to write .js in the path
- require() can be called anywhere (even inside if statements)
- Circular dependencies are allowed (but can be confusing)
Chapter 4 – ES Modules – The Modern Way (recommended in 2026)
Step 1: Enable ESM in your project
Add this line to package.json:
|
0 1 2 3 4 5 6 7 8 9 10 11 |
{ "name": "my-project", "type": "module", // ← This is the key line "main": "index.js", ... } |
Without “type”: “module”, Node.js treats .js files as CommonJS.
Step 2: Writing an ES Module
utils/math.js
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// Named exports (most common and recommended) export const PI = 3.14159265359; export function add(a, b) { return a + b; } export function subtract(a, b) { return a - b; } // Default export (one main thing per file) export default { add, subtract, PI }; // You can also export classes, constants, async functions, etc. export class Calculator { multiply(a, b) { return a * b; } } |
Step 3: Importing ES Modules
index.js
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// Option 1: Named imports (very clean) import { add, subtract, PI } from './utils/math.js'; console.log("PI:", PI); console.log("6 + 9 =", add(6, 9)); console.log("15 - 7 =", subtract(15, 7)); // Option 2: Import everything as an object import math from './utils/math.js'; console.log(math.add(10, 20)); // 30 console.log(math.PI); // 3.14159265359 // Option 3: Import class import { Calculator } from './utils/math.js'; const calc = new Calculator(); console.log(calc.multiply(7, 8)); // 56 |
Very important rules for ESM:
- You usually need to write the .js extension./utils/math.js ← correct ./utils/math ← usually does NOT work
- import must be at top level (cannot be inside if/for blocks)
- Dynamic import is possible with await import()
Chapter 5 – Real-world folder structure example (modern style)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
src/ ├── index.js ← entry point ├── config/ │ └── index.js ├── utils/ │ ├── date-utils.js │ ├── string-utils.js │ └── math.js ├── services/ │ ├── user-service.js │ └── email-service.js ├── controllers/ │ └── user-controller.js ├── routes/ │ └── user-routes.js └── middleware/ └── auth.js |
Example: user-service.js
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// src/services/user-service.js import { prisma } from '../config/database.js'; export async function findUserById(id) { return prisma.user.findUnique({ where: { id }, select: { id: true, email: true, name: true, createdAt: true } }); } export async function createUser(data) { return prisma.user.create({ data: { email: data.email, name: data.name, password: data.passwordHash } }); } |
Using it in controller:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// src/controllers/user-controller.js import * as userService from '../services/user-service.js'; export const getUser = async (req, res) => { const user = await userService.findUserById(req.params.id); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json(user); }; |
Chapter 6 – Quick Comparison Table (CommonJS vs ESM)
| Feature | CommonJS | ES Modules | Winner 2026 |
|---|---|---|---|
| Syntax | require / module.exports | import / export | ESM |
| File extension in import | Optional | Usually required | — |
| Top-level await | Not allowed | Allowed | ESM |
| Dynamic import | require() anywhere | await import() (inside async functions) | CommonJS |
| Tree-shaking | Poor | Excellent | ESM |
| Circular dependencies | Allowed (sometimes tricky) | Allowed but stricter | — |
| Modern libraries | Less common now | Most new libraries | ESM |
Chapter 7 – Common Mistakes & Best Practices (2026 style)
Common mistakes:
- Forgetting “type”: “module” → SyntaxError: Cannot use import statement outside a module
- Forgetting .js extension in ESM imports
- Using require in ESM project (or vice versa)
- Putting import inside conditions/loops (not allowed in ESM)
- Exporting nothing → undefined when imported
Best practices 2025–2026:
- Use ESM for all new projects
- Use named exports most of the time
- Use default export when there is clearly one main thing per file
- Keep modules small (ideally < 200–300 lines)
- Use barrel files carefully (index.js that re-exports everything)
- Always prefer relative paths (../utils) over absolute paths unless you have alias configured
Homework / Practice Suggestions
- Create a small project with 5–6 modules (utils, services, controllers)
- Write it first in CommonJS, then convert it to ESM
- Try both named exports and default exports
- Intentionally make a circular dependency and see what happens
- Create a module that exports both functions and a class
Let me know which part you want to go deeper into next:
- Converting CommonJS → ESM step-by-step
- Barrel files (index.js re-exports)
- Dynamic import (await import())
- Publishing dual CommonJS + ESM packages
- Real folder structure for medium/large project
- Handling circular dependencies
Just tell me what you want to explore next — I’ll continue with full examples. 😄
