Because sometimes your modules need to be more flexible than a yoga instructor! 🧘♂️
Ever found yourself in "require hell" where your Node.js modules are so tightly coupled they might as well be married? Or maybe you've tried to test a function that depends on 17 different external services and your test file looks like a spaghetti monster?
This GIST provides two battle-tested patterns for implementing Dependency Injection with the Factory Pattern in Node.js. No fancy frameworks required - just good ol' JavaScript closures doing what they do best!
- Tight Coupling: When your modules are hardwired to specific dependencies
- Jest Mocking Insanity: Ever spent 3 hours trying to mock a module that imports another module that imports another module? Yeah, we've all been there
- Testing Nightmares: When you can't test functions without spinning up a database, API server, and sacrificing a rubber duck
- Inflexibility: When swapping implementations requires rewriting half your codebase
- Private Function Testing: When you need to test internal logic without exposing it to the world
Both patterns solve the same core problems but offer different interfaces for different use cases. They both leverage JavaScript closures to create truly private functions and encapsulated dependencies - something that's surprisingly tricky in Node.js!
Returns an object with multiple methods - perfect for utility libraries or service modules.
// What you get:
{
exportedFunctionOne: Function,
exportedFunctionTwo: Function,
_privateFunctionTwo: Function // only in test mode!
}When to use: When you need a module that exposes multiple related functions as an API (think fs.readFile, fs.writeFile, etc.)
Returns a callable function with methods attached - ideal for single-purpose utilities.
// What you get:
exportedFunctionOne() // It's callable!
exportedFunctionOne._privateFunctionTwo // Also has methods in test modeWhen to use: When you have one primary function but might need helper methods (think express() which is callable but also has express.static)
Both patterns use the same clever tricks:
-
True Private Functions: Unlike class methods or object properties, functions defined inside the factory are truly private - they literally don't exist outside the closure scope unless explicitly exposed.
-
Two Levels of Privacy:
- Module-level private:
_privateFunctionOne- Shared across all instances, no access to instance dependencies - Instance-level private:
_privateFunctionTwo- Has access to injected dependencies through closure
- Module-level private:
-
Test-Mode Exposure: Private functions are conditionally exposed only when
NODE_ENV='test', keeping your production API clean while making testing possible.
// This is how the magic works:
function createFactory(dependencies) {
// This function is trapped in the closure!
function _privateFunctionTwo() {
// Can access 'dependencies' here thanks to closure
// This is TRULY private unless we explicitly expose it
}
// Only expose in test mode
return {
publicMethod,
...(process.env.NODE_ENV === 'test' ? { _privateFunctionTwo } : {})
};
}Without this pattern, you'd have to either:
- Make everything public (bye bye encapsulation!)
- Use underscores and hope nobody calls
_privatemethods (spoiler: they will) - Try to test private methods through public interfaces only (good luck with complex logic!)
Both patterns follow the same core principles:
- Factory Function: Creates instances with custom dependencies
- Dependency Validation: Ensures required dependencies are provided
- Partial Overrides: Mix and match custom and default dependencies
- Test Mode Magic: Exposes private functions only when
NODE_ENV='test' - Closure Power: Each instance maintains its own dependency scope
// Using the default instance (most common)
const utils = require('./factory-object-return');
utils.exportedFunctionOne(); // Uses default dependencies
utils.exportedFunctionTwo(); // Ready to rock!
// Or for the function variant
const doTheThing = require('./factory-function-return');
doTheThing(); // Just call it!// Need custom behavior? Use the factory!
const { factory } = require('./factory-object-return');
// Create your custom dependencies
const customDatabase = {
A: new PostgresClient(),
B: new RedisCache(),
C: new Logger('production'),
D: new MetricsCollector()
};
const customServices = {
E: new EmailService('SendGrid'),
F: new PaymentProcessor('Stripe')
};
// Create your custom instance
const customUtils = factory(customDatabase, customServices);
customUtils.exportedFunctionOne(); // Now using YOUR dependencies!// In your test file
process.env.NODE_ENV = 'test';
const { factory } = require('./factory-object-return');
describe('My Awesome Module', () => {
it('should test private functions', () => {
// Create test instance with mocked dependencies
const testInstance = factory(
{
A: mockDatabase,
B: mockCache,
C: mockLogger,
D: mockMetrics
},
{
E: mockEmailService,
F: mockPaymentProcessor
}
);
// Now you can test private functions!
expect(testInstance._privateFunctionTwo()).toBe('awesome');
// Module-level private functions are also available
const module = require('./factory-object-return');
expect(module._privateFunctionOne()).toBe('also awesome');
});
});// someModule.js - tightly coupled nightmare
const database = require('./database');
const emailService = require('./emailService');
const logger = require('./logger');
function processUser(userId) {
const user = database.getUser(userId);
logger.log('Processing user:', userId);
if (user.isActive) {
emailService.sendWelcome(user.email);
}
return user;
}
// someModule.test.js - Welcome to mock hell! 👹
jest.mock('./database');
jest.mock('./emailService');
jest.mock('./logger');
const database = require('./database');
const emailService = require('./emailService');
const logger = require('./logger');
const { processUser } = require('./someModule');
describe('processUser with Jest mocks', () => {
beforeEach(() => {
jest.clearAllMocks();
// Oh, you wanted to reset the module? Here's more boilerplate!
jest.resetModules();
});
it('should send email to active users', () => {
// Setting up mocks is like solving a Rubik's cube blindfolded
const mockUser = { id: 1, isActive: true, email: 'test@example.com' };
database.getUser.mockReturnValue(mockUser);
processUser(1);
// Did it work? Who knows! Check your module import order!
expect(emailService.sendWelcome).toHaveBeenCalledWith('test@example.com');
// Oh, you changed the implementation? Time to rewrite ALL your mocks!
});
});// processUserModule.js - Using our DI pattern
const { factory } = require('./factory-object-return');
function createProcessUser(deps) {
const { database, emailService, logger } = deps;
return {
processUser(userId) {
const user = database.getUser(userId);
logger.log('Processing user:', userId);
if (user.isActive) {
emailService.sendWelcome(user.email);
}
return user;
}
};
}
// Export default instance and factory
module.exports = createProcessUser({
database: require('./database'),
emailService: require('./emailService'),
logger: require('./logger')
});
module.exports.factory = createProcessUser;
// processUser.test.js - Testing bliss! 🌈
const { factory } = require('./processUserModule');
describe('processUser with DI', () => {
it('should send email to active users', () => {
// Create simple mock objects - no Jest magic needed!
const mockUser = { id: 1, isActive: true, email: 'test@example.com' };
const mockDeps = {
database: {
getUser: jest.fn().mockReturnValue(mockUser)
},
emailService: {
sendWelcome: jest.fn()
},
logger: {
log: jest.fn()
}
};
// Create instance with mocks - so clean!
const { processUser } = factory(mockDeps);
processUser(1);
// Assertions are straightforward and predictable
expect(mockDeps.emailService.sendWelcome).toHaveBeenCalledWith('test@example.com');
expect(mockDeps.logger.log).toHaveBeenCalledWith('Processing user:', 1);
});
it('should not send email to inactive users', () => {
// Different test? Just create different mocks!
const mockUser = { id: 2, isActive: false, email: 'inactive@example.com' };
const mockDeps = {
database: {
getUser: jest.fn().mockReturnValue(mockUser)
},
emailService: {
sendWelcome: jest.fn()
},
logger: {
log: jest.fn()
}
};
const { processUser } = factory(mockDeps);
processUser(2);
expect(mockDeps.emailService.sendWelcome).not.toHaveBeenCalled();
});
});- No Module Mocking: No more
jest.mock()at the top of every test file - No Import Order Issues: Jest mocks are hoisted and can cause bizarre issues
- Explicit Dependencies: You see exactly what each function needs
- Easy Test Isolation: Each test can have completely different mocks
- No Global State: No need for
beforeEachwithjest.clearAllMocks() - Refactoring Freedom: Change your dependencies without breaking tests
- Zero Dependencies: No DI frameworks needed
- Type Safe-ish: Validates required dependencies at runtime
- Test Friendly: Private functions accessible in test mode
- Flexible: Use defaults or inject custom dependencies
- Clean API: Production code stays clean, test helpers only appear in tests
- Object Return: When you need multiple equally important methods
- Function Return: When you have one primary function with helper methods
- Always provide all required properties (A, B, C, D) together - it's all or nothing!
- Optional properties (E, F) can be overridden individually
- Each factory instance maintains its own closure scope - no shared state surprises
- The default instance is created at module load time for convenience
This code follows some opinionated standards:
- Single quotes only (because we're not monsters)
- No semicolons (they're so 2010)
- Spaces around operators:
a + bnota+b - Spaces inside object braces:
{ key: value }not{key: value}
These patterns let you write testable, flexible Node.js modules without selling your soul to a DI framework. Pick the pattern that fits your use case, inject your dependencies, and test with confidence!
Happy coding! 🚀