12 minlesson

Module Design Patterns

Module Design Patterns

Learn common patterns for organizing modules in real applications.

Dependency Injection

Pass dependencies instead of hardcoding them:

javascript
1// BAD: Hardcoded dependency
2// userService.js
3import { PostgresDB } from './postgres.js';
4
5class UserService {
6 constructor() {
7 this.db = new PostgresDB(); // Tightly coupled!
8 }
9}
10
11// GOOD: Injected dependency
12// userService.js
13class UserService {
14 constructor(database) {
15 this.db = database;
16 }
17
18 async getUser(id) {
19 return this.db.findOne('users', id);
20 }
21}
22
23export { UserService };
javascript
1// main.js - Compose at the top level
2import { UserService } from './userService.js';
3import { PostgresDB } from './postgres.js';
4import { MockDB } from './mockDb.js';
5
6// Production
7const db = new PostgresDB(process.env.DATABASE_URL);
8const userService = new UserService(db);
9
10// Testing
11const mockDb = new MockDB();
12const testUserService = new UserService(mockDb);

Configuration Module

Centralize configuration:

javascript
1// config.js
2const 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};
19
20const env = process.env.NODE_ENV || 'development';
21
22export default config[env];
23export { config }; // Export all for special cases
javascript
1// api.js
2import config from './config.js';
3
4const response = await fetch(`${config.apiUrl}/users`);

Service Layer Pattern

Abstract business logic into services:

javascript
1// services/userService.js
2import { api } from '../api/client.js';
3import { validateEmail } from '../utils/validation.js';
4
5export 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 },
12
13 async getById(id) {
14 return api.get(`/users/${id}`);
15 },
16
17 async update(id, updates) {
18 return api.patch(`/users/${id}`, updates);
19 },
20
21 async delete(id) {
22 return api.delete(`/users/${id}`);
23 }
24};
javascript
1// Usage in component
2import { userService } from '../services/userService.js';
3
4const user = await userService.getById(123);
5await userService.update(123, { name: 'New Name' });

Repository Pattern

Abstract data access:

javascript
1// repositories/userRepository.js
2class UserRepository {
3 constructor(db) {
4 this.db = db;
5 this.collection = 'users';
6 }
7
8 async findById(id) {
9 return this.db.findOne(this.collection, { id });
10 }
11
12 async findByEmail(email) {
13 return this.db.findOne(this.collection, { email });
14 }
15
16 async findAll(filter = {}) {
17 return this.db.find(this.collection, filter);
18 }
19
20 async create(user) {
21 return this.db.insert(this.collection, user);
22 }
23
24 async update(id, updates) {
25 return this.db.update(this.collection, { id }, updates);
26 }
27
28 async delete(id) {
29 return this.db.delete(this.collection, { id });
30 }
31}
32
33export { UserRepository };

Module Composition

Build complex modules from simple ones:

javascript
1// features/auth/auth.validators.js
2export const validatePassword = (password) => {
3 return password.length >= 8;
4};
5
6export const validateUsername = (username) => {
7 return /^[a-zA-Z0-9_]{3,20}$/.test(username);
8};
javascript
1// features/auth/auth.service.js
2import { validatePassword, validateUsername } from './auth.validators.js';
3import { hashPassword, comparePassword } from './auth.crypto.js';
4import { userRepository } from './auth.repository.js';
5
6export 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 }
14
15 const hashedPassword = await hashPassword(password);
16 return userRepository.create({ username, password: hashedPassword });
17 },
18
19 async login(username, password) {
20 const user = await userRepository.findByUsername(username);
21 if (!user) {
22 throw new Error('User not found');
23 }
24
25 const valid = await comparePassword(password, user.password);
26 if (!valid) {
27 throw new Error('Invalid password');
28 }
29
30 return user;
31 }
32};
javascript
1// features/auth/index.js - Public API
2export { 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:

javascript
1// routes.js
2const 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};
8
9async function loadRoute(path) {
10 const loader = routes[path];
11 if (!loader) {
12 throw new Error('Route not found');
13 }
14
15 const module = await loader();
16 return module.default;
17}
18
19// Usage
20const Dashboard = await loadRoute('/dashboard');
21new Dashboard().render();

Plugin Architecture

Allow extending functionality:

javascript
1// core.js
2class App {
3 constructor() {
4 this.plugins = new Map();
5 }
6
7 use(name, plugin) {
8 if (typeof plugin.install === 'function') {
9 plugin.install(this);
10 }
11 this.plugins.set(name, plugin);
12 return this;
13 }
14
15 getPlugin(name) {
16 return this.plugins.get(name);
17 }
18}
19
20export default new App();
javascript
1// plugins/logger.js
2export 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};
javascript
1// main.js
2import app from './core.js';
3import { loggerPlugin } from './plugins/logger.js';
4
5app.use('logger', loggerPlugin);
6app.log('Application started');

Avoiding Circular Dependencies

Circular imports cause issues:

javascript
1// BAD: Circular dependency
2// a.js
3import { b } from './b.js';
4export const a = () => b();
5
6// b.js
7import { a } from './a.js'; // a is undefined at this point!
8export const b = () => a();

Solutions:

javascript
1// Solution 1: Restructure - extract shared code
2// shared.js
3export const shared = () => { /* ... */ };
4
5// a.js
6import { shared } from './shared.js';
7export const a = () => shared();
8
9// b.js
10import { shared } from './shared.js';
11export const b = () => shared();
javascript
1// Solution 2: Lazy loading
2// a.js
3export const a = async () => {
4 const { b } = await import('./b.js');
5 return b();
6};
javascript
1// Solution 3: Dependency injection
2// a.js
3export const createA = (b) => () => b();
4
5// b.js
6export const createB = (a) => () => a();
7
8// main.js
9import { createA } from './a.js';
10import { createB } from './b.js';
11
12const b = createB(() => { /* ... */ });
13const a = createA(b);

Testing Modules

Structure for testability:

javascript
1// utils/math.js
2export 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};
javascript
1// utils/math.test.js
2import { add, multiply, divide } from './math.js';
3
4describe('math utils', () => {
5 test('add', () => {
6 expect(add(2, 3)).toBe(5);
7 });
8
9 test('multiply', () => {
10 expect(multiply(2, 3)).toBe(6);
11 });
12
13 test('divide throws on zero', () => {
14 expect(() => divide(10, 0)).toThrow('Division by zero');
15 });
16});

Summary

PatternUse Case
Dependency InjectionLoose coupling, testability
Configuration ModuleCentralized settings
Service LayerBusiness logic abstraction
RepositoryData access abstraction
Lazy LoadingCode splitting, performance
Plugin ArchitectureExtensibility
Barrel FilesClean 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