Module Design Patterns
Learn common patterns for organizing modules in real applications.
Dependency Injection
Pass dependencies instead of hardcoding them:
javascript1// BAD: Hardcoded dependency2// userService.js3import { PostgresDB } from './postgres.js';45class UserService {6 constructor() {7 this.db = new PostgresDB(); // Tightly coupled!8 }9}1011// GOOD: Injected dependency12// userService.js13class UserService {14 constructor(database) {15 this.db = database;16 }1718 async getUser(id) {19 return this.db.findOne('users', id);20 }21}2223export { UserService };
javascript1// main.js - Compose at the top level2import { UserService } from './userService.js';3import { PostgresDB } from './postgres.js';4import { MockDB } from './mockDb.js';56// Production7const db = new PostgresDB(process.env.DATABASE_URL);8const userService = new UserService(db);910// Testing11const mockDb = new MockDB();12const testUserService = new UserService(mockDb);
Configuration Module
Centralize configuration:
javascript1// config.js2const config = {3 development: {4 apiUrl: 'http://localhost:3000',5 debug: true,6 logLevel: 'debug'7 },8 production: {9 apiUrl: 'https://api.example.com',10 debug: false,11 logLevel: 'error'12 },13 test: {14 apiUrl: 'http://localhost:3001',15 debug: true,16 logLevel: 'silent'17 }18};1920const env = process.env.NODE_ENV || 'development';2122export default config[env];23export { config }; // Export all for special cases
javascript1// api.js2import config from './config.js';34const response = await fetch(`${config.apiUrl}/users`);
Service Layer Pattern
Abstract business logic into services:
javascript1// services/userService.js2import { api } from '../api/client.js';3import { validateEmail } from '../utils/validation.js';45export const userService = {6 async create(userData) {7 if (!validateEmail(userData.email)) {8 throw new Error('Invalid email');9 }10 return api.post('/users', userData);11 },1213 async getById(id) {14 return api.get(`/users/${id}`);15 },1617 async update(id, updates) {18 return api.patch(`/users/${id}`, updates);19 },2021 async delete(id) {22 return api.delete(`/users/${id}`);23 }24};
javascript1// Usage in component2import { userService } from '../services/userService.js';34const user = await userService.getById(123);5await userService.update(123, { name: 'New Name' });
Repository Pattern
Abstract data access:
javascript1// repositories/userRepository.js2class UserRepository {3 constructor(db) {4 this.db = db;5 this.collection = 'users';6 }78 async findById(id) {9 return this.db.findOne(this.collection, { id });10 }1112 async findByEmail(email) {13 return this.db.findOne(this.collection, { email });14 }1516 async findAll(filter = {}) {17 return this.db.find(this.collection, filter);18 }1920 async create(user) {21 return this.db.insert(this.collection, user);22 }2324 async update(id, updates) {25 return this.db.update(this.collection, { id }, updates);26 }2728 async delete(id) {29 return this.db.delete(this.collection, { id });30 }31}3233export { UserRepository };
Module Composition
Build complex modules from simple ones:
javascript1// features/auth/auth.validators.js2export const validatePassword = (password) => {3 return password.length >= 8;4};56export const validateUsername = (username) => {7 return /^[a-zA-Z0-9_]{3,20}$/.test(username);8};
javascript1// features/auth/auth.service.js2import { validatePassword, validateUsername } from './auth.validators.js';3import { hashPassword, comparePassword } from './auth.crypto.js';4import { userRepository } from './auth.repository.js';56export const authService = {7 async register(username, password) {8 if (!validateUsername(username)) {9 throw new Error('Invalid username');10 }11 if (!validatePassword(password)) {12 throw new Error('Password too weak');13 }1415 const hashedPassword = await hashPassword(password);16 return userRepository.create({ username, password: hashedPassword });17 },1819 async login(username, password) {20 const user = await userRepository.findByUsername(username);21 if (!user) {22 throw new Error('User not found');23 }2425 const valid = await comparePassword(password, user.password);26 if (!valid) {27 throw new Error('Invalid password');28 }2930 return user;31 }32};
javascript1// features/auth/index.js - Public API2export { authService } from './auth.service.js';3export { validatePassword, validateUsername } from './auth.validators.js';4// Don't export internal modules like auth.crypto.js
Lazy Loading Modules
Load modules only when needed:
javascript1// routes.js2const routes = {3 '/': () => import('./pages/Home.js'),4 '/about': () => import('./pages/About.js'),5 '/dashboard': () => import('./pages/Dashboard.js'),6 '/admin': () => import('./pages/Admin.js')7};89async function loadRoute(path) {10 const loader = routes[path];11 if (!loader) {12 throw new Error('Route not found');13 }1415 const module = await loader();16 return module.default;17}1819// Usage20const Dashboard = await loadRoute('/dashboard');21new Dashboard().render();
Plugin Architecture
Allow extending functionality:
javascript1// core.js2class App {3 constructor() {4 this.plugins = new Map();5 }67 use(name, plugin) {8 if (typeof plugin.install === 'function') {9 plugin.install(this);10 }11 this.plugins.set(name, plugin);12 return this;13 }1415 getPlugin(name) {16 return this.plugins.get(name);17 }18}1920export default new App();
javascript1// plugins/logger.js2export const loggerPlugin = {3 install(app) {4 app.log = (msg) => console.log(`[${new Date().toISOString()}] ${msg}`);5 app.error = (msg) => console.error(`[ERROR] ${msg}`);6 }7};
javascript1// main.js2import app from './core.js';3import { loggerPlugin } from './plugins/logger.js';45app.use('logger', loggerPlugin);6app.log('Application started');
Avoiding Circular Dependencies
Circular imports cause issues:
javascript1// BAD: Circular dependency2// a.js3import { b } from './b.js';4export const a = () => b();56// b.js7import { a } from './a.js'; // a is undefined at this point!8export const b = () => a();
Solutions:
javascript1// Solution 1: Restructure - extract shared code2// shared.js3export const shared = () => { /* ... */ };45// a.js6import { shared } from './shared.js';7export const a = () => shared();89// b.js10import { shared } from './shared.js';11export const b = () => shared();
javascript1// Solution 2: Lazy loading2// a.js3export const a = async () => {4 const { b } = await import('./b.js');5 return b();6};
javascript1// Solution 3: Dependency injection2// a.js3export const createA = (b) => () => b();45// b.js6export const createB = (a) => () => a();78// main.js9import { createA } from './a.js';10import { createB } from './b.js';1112const b = createB(() => { /* ... */ });13const a = createA(b);
Testing Modules
Structure for testability:
javascript1// utils/math.js2export const add = (a, b) => a + b;3export const multiply = (a, b) => a * b;4export const divide = (a, b) => {5 if (b === 0) throw new Error('Division by zero');6 return a / b;7};
javascript1// utils/math.test.js2import { add, multiply, divide } from './math.js';34describe('math utils', () => {5 test('add', () => {6 expect(add(2, 3)).toBe(5);7 });89 test('multiply', () => {10 expect(multiply(2, 3)).toBe(6);11 });1213 test('divide throws on zero', () => {14 expect(() => divide(10, 0)).toThrow('Division by zero');15 });16});
Summary
| Pattern | Use Case |
|---|---|
| Dependency Injection | Loose coupling, testability |
| Configuration Module | Centralized settings |
| Service Layer | Business logic abstraction |
| Repository | Data access abstraction |
| Lazy Loading | Code splitting, performance |
| Plugin Architecture | Extensibility |
| Barrel Files | Clean import paths |
Best practices:
- Keep modules focused (single responsibility)
- Export a clear public API
- Hide implementation details
- Avoid circular dependencies
- Use dependency injection for testability