Node.js uses a module system to keep code organised. Instead of putting everything in one giant file, you split your application into separate files โ each responsible for one thing โ and connect them using require and module.exports. This is the CommonJS module system, and it is what Node.js uses by default. Every file you create in your Express server is a module. Understanding how modules work is fundamental to reading and writing any Node.js code, and it determines how you structure your entire MERN backend.
What Is a Module?
In Node.js, every .js file is automatically treated as a module. Each module has its own private scope โ variables and functions defined in one file are not visible to other files unless you explicitly export them. This prevents naming conflicts and makes each file self-contained and testable.
| Concept | What It Does | Keyword |
|---|---|---|
| Export | Make something available to other files | module.exports |
| Import | Load an export from another file or package | require() |
| Local module | A file you wrote in your project | require('./path/to/file') |
| Core module | Built into Node.js โ no install needed | require('fs'), require('path') |
| npm package | Installed via npm into node_modules | require('express') |
require / module.exports) and ES Modules (import / export). CommonJS is the default for Node.js and is what all Express tutorials use. ES Modules are the standard in React (Vite). In this series we use CommonJS on the server and ES Modules on the client โ do not mix them in the same project without extra configuration../ (current directory) or ../ (parent directory). Without a leading dot, Node.js assumes you are requiring a core module or an npm package, not a local file. For example: require('./routes/posts') loads your local file, while require('express') loads the npm package.require() is synchronous and caches modules after the first load. If you require the same file from ten different places, Node.js runs that file once and reuses the cached export for all subsequent calls. This is usually the behaviour you want โ but it means if a module has side effects (like opening a database connection), those side effects only run once, not once per require call.Exporting and Importing โ Basic Patterns
// โโ Exporting a single value โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// server/src/config/db.js
const mongoose = require('mongoose');
const connectDB = async () => {
await mongoose.connect(process.env.MONGODB_URI);
console.log('MongoDB connected');
};
module.exports = connectDB; // export a single function
// โโ Importing a single value โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// server/index.js
const connectDB = require('./src/config/db'); // .js extension is optional
connectDB();
// โโ Exporting multiple values โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// server/src/utils/helpers.js
const slugify = (text) => text.toLowerCase().replace(/\s+/g, '-');
const capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);
module.exports = { slugify, capitalize }; // export an object
// โโ Importing multiple values โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const { slugify, capitalize } = require('./utils/helpers');
console.log(slugify('Hello World')); // hello-world
console.log(capitalize('mern stack')); // Mern stack
The require() Resolution Algorithm
When you call require('something'), Node.js resolves it in this order:
1. Is it a core module? (fs, path, http, crypto...)
โ Load the built-in module immediately
2. Does the path start with ./ or ../ or / ?
โ It is a local file โ resolve relative to the current file
โ Try: something.js โ something.json โ something/index.js
3. Otherwise โ look in node_modules:
โ ./node_modules/something
โ ../node_modules/something
โ ../../node_modules/something (walks up until root)
โ Throw MODULE_NOT_FOUND if not found anywhere
Structuring a MERN Server with Modules
server/
โโโ index.js โ requires config/db, routes, middleware
โโโ src/
โ โโโ config/
โ โ โโโ db.js โ exports connectDB function
โ โโโ models/
โ โ โโโ Post.js โ exports Post Mongoose model
โ โโโ controllers/
โ โ โโโ postController.js โ exports { getAllPosts, createPost, ... }
โ โโโ routes/
โ โ โโโ posts.js โ exports Express Router
โ โโโ middleware/
โ โโโ auth.js โ exports protect middleware function
// โโ How the modules connect โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// server/src/controllers/postController.js
const Post = require('../models/Post'); // โ requires the model
const getAllPosts = async (req, res) => {
const posts = await Post.find();
res.json({ success: true, data: posts });
};
module.exports = { getAllPosts };
// server/src/routes/posts.js
const express = require('express');
const router = express.Router();
const { getAllPosts } = require('../controllers/postController'); // โ requires controller
router.get('/', getAllPosts);
module.exports = router; // โ exports the router
// server/index.js
const express = require('express');
const app = express();
app.use('/api/posts', require('./src/routes/posts')); // โ requires the router
Core Modules You Will Use in MERN
| Module | require() call | Used For in MERN |
|---|---|---|
| path | require('path') |
Building file paths for uploads, static files |
| fs / fs/promises | require('fs/promises') |
Reading config, writing logs, file operations |
| crypto | require('crypto') |
Generating random tokens for password resets, email verify |
| os | require('os') |
Getting temp directory for file uploads |
| http | require('http') |
Creating HTTP server (Express wraps this) |
| events | require('events') |
Custom event emitters โ used internally by many packages |
Common Mistakes
Mistake 1 โ Wrong path in require()
โ Wrong โ forgetting the leading dot on a local file path:
const connectDB = require('src/config/db');
// Error: Cannot find module 'src/config/db'
// Node thinks it is an npm package, not a local file
โ
Correct โ always use ./ or ../ for local files:
const connectDB = require('./src/config/db'); // relative to current file โ
Mistake 2 โ Circular dependencies
โ Wrong โ file A requires file B, and file B requires file A:
userController.js โ requires โ postModel.js
postModel.js โ requires โ userController.js โ circular!
// Node.js will partially load one of them โ hard-to-debug undefined errors
โ Correct โ restructure so data flows in one direction. Models should never require controllers. Controllers require models โ not the other way around.
Mistake 3 โ Modifying module.exports after assignment
โ Wrong โ assigning to module.exports then trying to add properties to exports:
module.exports = connectDB; // replaces exports with a function
exports.helper = someHelper; // ignored โ exports no longer === module.exports
โ
Correct โ pick one style per file. Either assign to module.exports directly, or add all properties to the exports shorthand โ never mix them:
module.exports = { connectDB, helper: someHelper }; // clean single assignment โ
Quick Reference
| Task | Code |
|---|---|
| Export one thing | module.exports = myFunction |
| Export multiple things | module.exports = { fn1, fn2, CONSTANT } |
| Import local file | const x = require('./path/file') |
| Import core module | const fs = require('fs/promises') |
| Import npm package | const express = require('express') |
| Destructure import | const { fn1, fn2 } = require('./helpers') |