
How to Create a Node.js Module
Creating custom Node.js modules is a fundamental skill that lets you organize your code better, share functionality across projects, and contribute to the vibrant npm ecosystem. Whether you’re building internal utilities for your team or publishing the next must-have package, understanding how to properly structure, test, and distribute Node.js modules will make you a more effective developer. This guide walks you through everything from basic module creation to advanced publishing strategies, complete with real examples and troubleshooting tips you’ll actually use.
How Node.js Modules Work Under the Hood
Node.js modules follow the CommonJS specification, where each file is treated as a separate module with its own scope. When you use require()
or import
, Node.js goes through a resolution algorithm to find your module, loads it, caches it, and returns the exported functionality.
The module system uses three main concepts:
- module.exports – The object that gets returned when another file requires your module
- exports – A shorthand reference to module.exports (be careful with reassignment)
- require() – The function that loads and returns other modules
Here’s what happens when you run require('./mymodule')
:
// Node.js wraps your module code like this:
(function(exports, require, module, __filename, __dirname) {
// Your module code here
return module.exports;
});
Step-by-Step Module Creation Guide
Let’s build a practical utility module for handling API rate limiting. This example covers most patterns you’ll encounter in real projects.
Basic Module Structure
Start by creating your module file rate-limiter.js
:
// rate-limiter.js
class RateLimiter {
constructor(maxRequests = 100, windowMs = 60000) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.requests = new Map();
}
isAllowed(identifier) {
const now = Date.now();
const userRequests = this.requests.get(identifier) || [];
// Remove old requests outside the window
const validRequests = userRequests.filter(
timestamp => now - timestamp < this.windowMs
);
if (validRequests.length >= this.maxRequests) {
return false;
}
validRequests.push(now);
this.requests.set(identifier, validRequests);
return true;
}
reset(identifier) {
this.requests.delete(identifier);
}
getStats(identifier) {
const userRequests = this.requests.get(identifier) || [];
const now = Date.now();
const validRequests = userRequests.filter(
timestamp => now - timestamp < this.windowMs
);
return {
requests: validRequests.length,
maxRequests: this.maxRequests,
resetTime: now + this.windowMs
};
}
}
// Export the class
module.exports = RateLimiter;
// Alternative: export multiple things
// module.exports = {
// RateLimiter,
// createLimiter: (max, window) => new RateLimiter(max, window)
// };
Using Your Module
Now create a test file to use your module:
// app.js
const RateLimiter = require('./rate-limiter');
const limiter = new RateLimiter(5, 10000); // 5 requests per 10 seconds
// Simulate API requests
for (let i = 0; i < 7; i++) {
const allowed = limiter.isAllowed('user123');
console.log(`Request ${i + 1}: ${allowed ? 'Allowed' : 'Rate limited'}`);
}
// Check stats
console.log(limiter.getStats('user123'));
Adding ES6 Module Support
Modern Node.js also supports ES6 modules. Create a package.json
with "type": "module"
and update your exports:
// rate-limiter.mjs (or .js with "type": "module" in package.json)
export default class RateLimiter {
// ... same implementation
}
// Named exports
export const createLimiter = (max, window) => new RateLimiter(max, window);
export const DEFAULT_WINDOW = 60000;
// Using ES6 imports
import RateLimiter, { createLimiter } from './rate-limiter.mjs';
const limiter = createLimiter(10, 5000);
Real-World Examples and Use Cases
Express.js Middleware Module
Here's how to create a reusable Express middleware using our rate limiter:
// middleware/rate-limit-middleware.js
const RateLimiter = require('../rate-limiter');
function createRateLimitMiddleware(options = {}) {
const limiter = new RateLimiter(
options.maxRequests || 100,
options.windowMs || 60000
);
return (req, res, next) => {
const identifier = options.keyGenerator
? options.keyGenerator(req)
: req.ip;
if (!limiter.isAllowed(identifier)) {
const stats = limiter.getStats(identifier);
res.status(429).json({
error: 'Too many requests',
retryAfter: Math.ceil((stats.resetTime - Date.now()) / 1000)
});
return;
}
next();
};
}
module.exports = createRateLimitMiddleware;
Database Connection Pool Module
Another common pattern is creating modules that manage resources:
// db-pool.js
const mysql = require('mysql2/promise');
class DatabasePool {
constructor(config) {
this.pool = mysql.createPool({
host: config.host || 'localhost',
user: config.user,
password: config.password,
database: config.database,
waitForConnections: true,
connectionLimit: config.connectionLimit || 10,
queueLimit: 0
});
}
async query(sql, params) {
try {
const [rows] = await this.pool.execute(sql, params);
return rows;
} catch (error) {
console.error('Database query error:', error);
throw error;
}
}
async close() {
await this.pool.end();
}
getConnectionCount() {
return {
all: this.pool.pool._allConnections.length,
free: this.pool.pool._freeConnections.length,
used: this.pool.pool._acquiringConnections.length
};
}
}
module.exports = DatabasePool;
Publishing Your Module to npm
Package.json Configuration
Create a proper package.json
for your module:
{
"name": "@yourname/rate-limiter",
"version": "1.0.0",
"description": "Simple in-memory rate limiter for Node.js applications",
"main": "rate-limiter.js",
"scripts": {
"test": "jest",
"lint": "eslint *.js",
"prepublishOnly": "npm test && npm run lint"
},
"keywords": ["rate-limiting", "api", "middleware", "express"],
"author": "Your Name ",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/yourname/rate-limiter.git"
},
"engines": {
"node": ">=12.0.0"
},
"devDependencies": {
"jest": "^29.0.0",
"eslint": "^8.0.0"
}
}
Testing Your Module
Add comprehensive tests using Jest:
// rate-limiter.test.js
const RateLimiter = require('./rate-limiter');
describe('RateLimiter', () => {
let limiter;
beforeEach(() => {
limiter = new RateLimiter(3, 1000); // 3 requests per second
});
test('allows requests within limit', () => {
expect(limiter.isAllowed('user1')).toBe(true);
expect(limiter.isAllowed('user1')).toBe(true);
expect(limiter.isAllowed('user1')).toBe(true);
});
test('blocks requests over limit', () => {
// Use up the limit
for (let i = 0; i < 3; i++) {
limiter.isAllowed('user1');
}
expect(limiter.isAllowed('user1')).toBe(false);
});
test('resets after time window', async () => {
// Use up the limit
for (let i = 0; i < 3; i++) {
limiter.isAllowed('user1');
}
expect(limiter.isAllowed('user1')).toBe(false);
// Wait for window to reset
await new Promise(resolve => setTimeout(resolve, 1100));
expect(limiter.isAllowed('user1')).toBe(true);
});
test('tracks different users separately', () => {
for (let i = 0; i < 3; i++) {
limiter.isAllowed('user1');
}
expect(limiter.isAllowed('user1')).toBe(false);
expect(limiter.isAllowed('user2')).toBe(true);
});
});
Publishing Commands
# Login to npm
npm login
# Publish your module
npm publish
# For scoped packages (recommended for personal projects)
npm publish --access public
Comparison with Alternative Approaches
Approach | Pros | Cons | Best For |
---|---|---|---|
Single File Module | Simple, fast loading | Limited functionality | Utilities, helpers |
Multi-file Package | Better organization | More complex setup | Libraries, frameworks |
TypeScript Module | Type safety, better tooling | Compilation step required | Large projects, teams |
Native ESM | Modern standard, tree-shaking | Limited backward compatibility | New projects, bundlers |
Best Practices and Common Pitfalls
Export Patterns
Choose the right export pattern for your use case:
// ❌ Don't reassign exports
exports = { myFunction }; // This won't work!
// ✅ Use module.exports for reassignment
module.exports = { myFunction };
// ✅ Add properties to exports
exports.myFunction = myFunction;
exports.myOtherFunction = myOtherFunction;
// ✅ Export a class
module.exports = MyClass;
// ✅ Export both class and factory function
module.exports = MyClass;
module.exports.create = (options) => new MyClass(options);
Handling Dependencies
Manage dependencies properly:
// config-loader.js - Example of graceful dependency handling
let yaml;
try {
yaml = require('yaml');
} catch (error) {
// yaml is optional dependency
}
function loadConfig(filePath) {
const fs = require('fs');
const path = require('path');
const ext = path.extname(filePath);
const content = fs.readFileSync(filePath, 'utf8');
switch (ext) {
case '.json':
return JSON.parse(content);
case '.yml':
case '.yaml':
if (!yaml) {
throw new Error('yaml package required for YAML files. Install with: npm install yaml');
}
return yaml.parse(content);
default:
throw new Error(`Unsupported config format: ${ext}`);
}
}
module.exports = { loadConfig };
Performance Considerations
Key performance tips for Node.js modules:
- Lazy loading - Only require modules when needed
- Caching - Store expensive computations
- Async patterns - Use promises/async-await for I/O operations
- Memory management - Clean up resources, avoid memory leaks
// Performance-optimized module
class OptimizedCache {
constructor(maxSize = 1000, ttl = 300000) {
this.cache = new Map();
this.maxSize = maxSize;
this.ttl = ttl;
this.timers = new Map();
}
set(key, value) {
// Implement LRU eviction
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
const firstKey = this.cache.keys().next().value;
this.delete(firstKey);
}
this.cache.set(key, {
value,
timestamp: Date.now()
});
// Set TTL timer
if (this.timers.has(key)) {
clearTimeout(this.timers.get(key));
}
const timer = setTimeout(() => this.delete(key), this.ttl);
this.timers.set(key, timer);
}
get(key) {
const item = this.cache.get(key);
if (!item) return undefined;
// Check if expired
if (Date.now() - item.timestamp > this.ttl) {
this.delete(key);
return undefined;
}
// Move to end (LRU)
this.cache.delete(key);
this.cache.set(key, item);
return item.value;
}
delete(key) {
this.cache.delete(key);
if (this.timers.has(key)) {
clearTimeout(this.timers.get(key));
this.timers.delete(key);
}
}
}
module.exports = OptimizedCache;
Security Best Practices
- Input validation - Always validate and sanitize inputs
- Avoid eval() - Never use eval() with user input
- Secure defaults - Make secure configuration the default
- Dependency auditing - Regularly audit dependencies with
npm audit
Common Troubleshooting Issues
Circular Dependencies
// ❌ Problematic circular dependency
// a.js
const b = require('./b');
module.exports = { name: 'a', b };
// b.js
const a = require('./a'); // This might be undefined!
module.exports = { name: 'b', a };
// ✅ Better approach - use dependency injection
// a.js
module.exports = function createA(b) {
return { name: 'a', b };
};
Module Resolution Issues
# Debug module resolution
node --trace-warnings app.js
NODE_DEBUG=module node app.js
For production deployments, consider using robust hosting solutions. VPS services provide the flexibility needed for Node.js applications, while dedicated servers offer the performance required for high-traffic module distributions.
Remember to consult the official Node.js modules documentation and npm module creation guide for the latest best practices and API changes. Creating effective Node.js modules is an iterative process - start simple, test thoroughly, and gradually add features based on real usage patterns.

This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.
This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.