12 Popular Design Patterns
Concise overview with short descriptions and tiny JavaScript examples.
1) Singleton (Creational)
Ensure a class has only one instance and provide a global access point.
Example
// === Node.js TypeScript ===
import { Pool, PoolClient } from 'pg';
class DatabaseService {
private static instance: DatabaseService | null = null;
private pool: Pool | null = null;
private isConnected = false;
private constructor() {
// Private constructor to prevent direct instantiation
}
static getInstance(): DatabaseService {
if (!DatabaseService.instance) {
DatabaseService.instance = new DatabaseService();
}
return DatabaseService.instance;
}
async connect(config: { host: string; port: number; database: string; user: string; password: string }): Promise {
if (this.isConnected) return;
try {
this.pool = new Pool(config);
// Test connection
const client = await this.pool.connect();
client.release();
this.isConnected = true;
} catch (error) {
throw new Error(`Database connection failed: ${error.message}`);
}
}
async query(sql: string, params?: any[]): Promise {
if (!this.pool) throw new Error('Database not connected. Call connect() first.');
try {
const result = await this.pool.query(sql, params);
return result.rows;
} catch (error) {
console.error('Query error:', error);
throw new Error(`Query execution failed: ${error.message}`);
}
}
async transaction(callback: (client: PoolClient) => Promise): Promise {
if (!this.pool) throw new Error('Database not connected');
const client = await this.pool.connect();
try {
await client.query('BEGIN');
const result = await callback(client);
await client.query('COMMIT');
return result;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
async disconnect(): Promise {
if (this.pool) {
await this.pool.end();
this.isConnected = false;
this.pool = null;
}
}
}
// Usage - Only one instance throughout the application
const db1 = DatabaseService.getInstance();
const db2 = DatabaseService.getInstance();
console.log(db1 === db2); // true - same instance
await db1.connect({
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME || 'mydb',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || ''
});
// Use in services
const users = await db1.query('SELECT * FROM users WHERE id = $1', [1]);
// === NestJS ===
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { Pool } from 'pg';
@Injectable()
export class DatabaseService implements OnModuleInit, OnModuleDestroy {
private pool: Pool;
async onModuleInit() {
this.pool = new Pool({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT),
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
});
}
async query(sql: string, params?: any[]): Promise {
const result = await this.pool.query(sql, params);
return result.rows;
}
async onModuleDestroy() {
await this.pool.end();
}
}
// NestJS providers are singleton-scoped by default
// All modules injecting DatabaseService get the same instance
2) Factory Method (Creational)
Define an interface for creating objects; subclasses decide which class to instantiate.
Example
// === Node.js TypeScript ===
import * as fs from 'fs/promises';
import * as path from 'path';
enum LogLevel {
DEBUG = 'DEBUG',
INFO = 'INFO',
WARN = 'WARN',
ERROR = 'ERROR'
}
interface ILogger {
log(level: LogLevel, message: string, meta?: Record): void;
info(message: string, meta?: Record): void;
error(message: string, error?: Error, meta?: Record): void;
}
class ConsoleLogger implements ILogger {
log(level: LogLevel, message: string, meta?: Record): void {
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
level,
message,
...meta
};
if (level === LogLevel.ERROR) {
console.error(JSON.stringify(logEntry));
} else {
console.log(JSON.stringify(logEntry));
}
}
info(message: string, meta?: Record): void {
this.log(LogLevel.INFO, message, meta);
}
error(message: string, error?: Error, meta?: Record): void {
this.log(LogLevel.ERROR, message, {
...meta,
error: error ? { message: error.message, stack: error.stack } : undefined
});
}
}
class FileLogger implements ILogger {
private logDir: string;
private logFile: string;
constructor(logDir: string = './logs') {
this.logDir = logDir;
this.logFile = path.join(logDir, `app-${new Date().toISOString().split('T')[0]}.log`);
}
private async ensureLogDir(): Promise {
try {
await fs.mkdir(this.logDir, { recursive: true });
} catch (error) {
console.error('Failed to create log directory:', error);
}
}
async log(level: LogLevel, message: string, meta?: Record): Promise {
await this.ensureLogDir();
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
level,
message,
...meta
};
try {
await fs.appendFile(this.logFile, JSON.stringify(logEntry) + '\n');
} catch (error) {
console.error('Failed to write log:', error);
}
}
info(message: string, meta?: Record): Promise {
return this.log(LogLevel.INFO, message, meta);
}
async error(message: string, error?: Error, meta?: Record): Promise {
await this.log(LogLevel.ERROR, message, {
...meta,
error: error ? { message: error.message, stack: error.stack } : undefined
});
}
}
class CloudLogger implements ILogger {
constructor(private apiKey: string, private endpoint: string) {}
async log(level: LogLevel, message: string, meta?: Record): Promise {
try {
await fetch(this.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
level,
message,
timestamp: new Date().toISOString(),
...meta
})
});
} catch (error) {
console.error('Failed to send log to cloud:', error);
}
}
info(message: string, meta?: Record): Promise {
return this.log(LogLevel.INFO, message, meta);
}
error(message: string, error?: Error, meta?: Record): Promise {
return this.log(LogLevel.ERROR, message, {
...meta,
error: error ? { message: error.message, stack: error.stack } : undefined
});
}
}
// Factory function - decides which logger to create based on environment
function createLogger(env: 'dev'|'prod'|'cloud', config?: { logDir?: string; apiKey?: string; endpoint?: string }): ILogger {
switch (env) {
case 'prod':
return new FileLogger(config?.logDir);
case 'cloud':
if (!config?.apiKey || !config?.endpoint) {
throw new Error('Cloud logger requires apiKey and endpoint');
}
return new CloudLogger(config.apiKey, config.endpoint);
case 'dev':
default:
return new ConsoleLogger();
}
}
// Usage
const logger = createLogger(
process.env.NODE_ENV === 'production'
? process.env.LOG_TYPE === 'cloud' ? 'cloud' : 'prod'
: 'dev',
{
logDir: process.env.LOG_DIR || './logs',
apiKey: process.env.CLOUD_LOG_API_KEY,
endpoint: process.env.CLOUD_LOG_ENDPOINT
}
);
logger.info('Server started', { port: 3000, env: process.env.NODE_ENV });
logger.error('Database connection failed', new Error('Connection timeout'), { host: 'localhost' });
// === NestJS ===
import { Injectable, Module } from '@nestjs/common';
@Injectable()
export class LoggerFactory {
create(env: 'dev'|'prod'|'cloud', config?: any): ILogger {
return createLogger(env, config);
}
}
const LOGGER_PROVIDER = {
provide: 'LOGGER',
useFactory: (): ILogger => {
const env = process.env.NODE_ENV === 'production'
? (process.env.LOG_TYPE === 'cloud' ? 'cloud' : 'prod')
: 'dev';
return createLogger(env, {
logDir: process.env.LOG_DIR,
apiKey: process.env.CLOUD_LOG_API_KEY,
endpoint: process.env.CLOUD_LOG_ENDPOINT
});
},
};
@Module({ providers: [LOGGER_PROVIDER, LoggerFactory], exports: ['LOGGER'] })
export class LoggingModule {}
3) Abstract Factory (Creational)
Produce families of related objects without specifying concrete classes.
Example
// === Node.js TypeScript ===
type User = { id: string; email: string; name: string; createdAt: Date };
type Order = { id: string; userId: string; total: number; status: string };
interface UserRepository {
findById(id: string): Promise;
findByEmail(email: string): Promise;
create(user: Omit): Promise;
update(id: string, data: Partial): Promise;
}
interface OrderRepository {
findByUserId(userId: string): Promise;
findById(id: string): Promise;
create(order: Omit): Promise;
}
interface IRepositoryFactory {
createUserRepository(): UserRepository;
createOrderRepository(): OrderRepository;
createTransaction(callback: () => Promise): Promise;
}
// PostgreSQL implementation
class SqlUserRepository implements UserRepository {
constructor(private db: any) {}
async findById(id: string): Promise {
const result = await this.db.query(
'SELECT * FROM users WHERE id = $1',
[id]
);
return result.rows[0] || null;
}
async findByEmail(email: string): Promise {
const result = await this.db.query(
'SELECT * FROM users WHERE email = $1',
[email]
);
return result.rows[0] || null;
}
async create(user: Omit): Promise {
const result = await this.db.query(
`INSERT INTO users (email, name, created_at)
VALUES ($1, $2, NOW()) RETURNING *`,
[user.email, user.name]
);
return result.rows[0];
}
async update(id: string, data: Partial): Promise {
const fields = Object.keys(data).map((key, i) => `${key} = $${i + 2}`).join(', ');
const result = await this.db.query(
`UPDATE users SET ${fields} WHERE id = $1 RETURNING *`,
[id, ...Object.values(data)]
);
if (!result.rows[0]) throw new Error('User not found');
return result.rows[0];
}
}
class SqlOrderRepository implements OrderRepository {
constructor(private db: any) {}
async findByUserId(userId: string): Promise {
const result = await this.db.query(
'SELECT * FROM orders WHERE user_id = $1 ORDER BY created_at DESC',
[userId]
);
return result.rows;
}
async findById(id: string): Promise {
const result = await this.db.query(
'SELECT * FROM orders WHERE id = $1',
[id]
);
return result.rows[0] || null;
}
async create(order: Omit): Promise {
const result = await this.db.query(
`INSERT INTO orders (user_id, total, status, created_at)
VALUES ($1, $2, $3, NOW()) RETURNING *`,
[order.userId, order.total, order.status]
);
return result.rows[0];
}
}
class SqlRepositoryFactory implements IRepositoryFactory {
constructor(private dbPool: any) {}
createUserRepository(): UserRepository {
return new SqlUserRepository(this.dbPool);
}
createOrderRepository(): OrderRepository {
return new SqlOrderRepository(this.dbPool);
}
async createTransaction(callback: () => Promise): Promise {
const client = await this.dbPool.connect();
try {
await client.query('BEGIN');
// Create repositories with transaction client
const userRepo = new SqlUserRepository(client);
const orderRepo = new SqlOrderRepository(client);
const result = await callback();
await client.query('COMMIT');
return result;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
}
// MongoDB implementation
class MongoUserRepository implements UserRepository {
constructor(private collection: any) {}
async findById(id: string): Promise {
return await this.collection.findOne({ _id: id });
}
async findByEmail(email: string): Promise {
return await this.collection.findOne({ email });
}
async create(user: Omit): Promise {
const doc = { ...user, _id: this.generateId(), createdAt: new Date() };
await this.collection.insertOne(doc);
return doc;
}
async update(id: string, data: Partial): Promise {
const result = await this.collection.findOneAndUpdate(
{ _id: id },
{ $set: data },
{ returnDocument: 'after' }
);
if (!result.value) throw new Error('User not found');
return result.value;
}
private generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
}
class MongoOrderRepository implements OrderRepository {
constructor(private collection: any) {}
async findByUserId(userId: string): Promise {
return await this.collection.find({ userId }).sort({ createdAt: -1 }).toArray();
}
async findById(id: string): Promise {
return await this.collection.findOne({ _id: id });
}
async create(order: Omit): Promise {
const doc = { ...order, _id: this.generateId() };
await this.collection.insertOne(doc);
return doc;
}
private generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
}
class MongoRepositoryFactory implements IRepositoryFactory {
constructor(private db: any) {}
createUserRepository(): UserRepository {
return new MongoUserRepository(this.db.collection('users'));
}
createOrderRepository(): OrderRepository {
return new MongoOrderRepository(this.db.collection('orders'));
}
async createTransaction(callback: () => Promise): Promise {
const session = this.db.startSession();
try {
session.startTransaction();
// Repositories would need session context
const result = await callback();
await session.commitTransaction();
return result;
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
}
}
// Factory function to create the appropriate factory
function createRepositoryFactory(db: 'sql'|'mongo', dbConnection: any): IRepositoryFactory {
return db === 'mongo'
? new MongoRepositoryFactory(dbConnection)
: new SqlRepositoryFactory(dbConnection);
}
// Usage
const dbType = process.env.DB_TYPE === 'mongo' ? 'mongo' : 'sql';
const dbConnection = dbType === 'mongo' ? mongoClient : pgPool;
const repoFactory = createRepositoryFactory(dbType, dbConnection);
const userRepo = repoFactory.createUserRepository();
const orderRepo = repoFactory.createOrderRepository();
// Use repositories
const user = await userRepo.findById('u1');
const orders = await orderRepo.findByUserId('u1');
// Transaction example
await repoFactory.createTransaction(async () => {
const newUser = await userRepo.create({ email: 'test@example.com', name: 'Test' });
await orderRepo.create({ userId: newUser.id, total: 100, status: 'pending' });
});
// === NestJS ===
import { Module, DynamicModule } from '@nestjs/common';
@Module({})
export class RepositoryModule {
static register(db: 'sql'|'mongo', dbConnection: any): DynamicModule {
const factory = createRepositoryFactory(db, dbConnection);
const providers = [
{
provide: 'REPOSITORY_FACTORY',
useValue: factory
},
{
provide: 'USER_REPOSITORY',
useFactory: (factory: IRepositoryFactory) => factory.createUserRepository(),
inject: ['REPOSITORY_FACTORY']
},
{
provide: 'ORDER_REPOSITORY',
useFactory: (factory: IRepositoryFactory) => factory.createOrderRepository(),
inject: ['REPOSITORY_FACTORY']
}
];
return {
module: RepositoryModule,
providers,
exports: providers
};
}
}
4) Builder (Creational)
Step-by-step construction for complex objects with a clear fluent API.
Example
// === Node.js TypeScript ===
interface HttpHeaders {
[key: string]: string | string[];
}
interface EmailMessage {
to: string[];
cc?: string[];
bcc?: string[];
subject: string;
body: string;
attachments?: Array<{ filename: string; content: Buffer; contentType?: string }>;
priority?: 'low' | 'normal' | 'high';
replyTo?: string;
}
class EmailMessageBuilder {
private to: string[] = [];
private cc: string[] = [];
private bcc: string[] = [];
private subject = '';
private body = '';
private attachments: Array<{ filename: string; content: Buffer; contentType?: string }> = [];
private priority: 'low' | 'normal' | 'high' = 'normal';
private replyTo?: string;
addRecipient(email: string): this {
if (!this.isValidEmail(email)) {
throw new Error(`Invalid email address: ${email}`);
}
this.to.push(email);
return this;
}
addRecipients(emails: string[]): this {
emails.forEach(email => this.addRecipient(email));
return this;
}
addCc(email: string): this {
if (!this.isValidEmail(email)) {
throw new Error(`Invalid email address: ${email}`);
}
this.cc.push(email);
return this;
}
addBcc(email: string): this {
if (!this.isValidEmail(email)) {
throw new Error(`Invalid email address: ${email}`);
}
this.bcc.push(email);
return this;
}
setSubject(subject: string): this {
if (!subject || subject.trim().length === 0) {
throw new Error('Subject cannot be empty');
}
this.subject = subject.trim();
return this;
}
setBody(body: string): this {
if (!body || body.trim().length === 0) {
throw new Error('Body cannot be empty');
}
this.body = body;
return this;
}
setHtmlBody(html: string): this {
if (!html || html.trim().length === 0) {
throw new Error('HTML body cannot be empty');
}
this.body = html;
return this;
}
addAttachment(filename: string, content: Buffer, contentType?: string): this {
if (!filename) {
throw new Error('Filename is required for attachment');
}
this.attachments.push({ filename, content, contentType });
return this;
}
setPriority(priority: 'low' | 'normal' | 'high'): this {
this.priority = priority;
return this;
}
setReplyTo(email: string): this {
if (!this.isValidEmail(email)) {
throw new Error(`Invalid reply-to email address: ${email}`);
}
this.replyTo = email;
return this;
}
build(): EmailMessage {
// Validation before building
if (this.to.length === 0) {
throw new Error('At least one recipient is required');
}
if (!this.subject) {
throw new Error('Subject is required');
}
if (!this.body) {
throw new Error('Body is required');
}
return {
to: [...this.to],
cc: this.cc.length > 0 ? [...this.cc] : undefined,
bcc: this.bcc.length > 0 ? [...this.bcc] : undefined,
subject: this.subject,
body: this.body,
attachments: this.attachments.length > 0 ? [...this.attachments] : undefined,
priority: this.priority,
replyTo: this.replyTo
};
}
private isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
}
// Usage example
const email = new EmailMessageBuilder()
.addRecipient('customer@example.com')
.addCc('manager@example.com')
.setSubject('Order Confirmation #12345')
.setHtmlBody(`
Thank you for your order!
Your order #12345 has been confirmed.
Total: $99.99
`)
.addAttachment('invoice.pdf', Buffer.from('pdf content...'), 'application/pdf')
.setPriority('high')
.setReplyTo('support@example.com')
.build();
// === NestJS ===
import { Injectable } from '@nestjs/common';
@Injectable()
export class EmailMessageBuilder {
// Same implementation as above
}
@Injectable()
export class EmailService {
constructor(private emailBuilder: EmailMessageBuilder) {}
async sendWelcomeEmail(userEmail: string, userName: string): Promise {
const email = this.emailBuilder
.addRecipient(userEmail)
.setSubject(`Welcome to our platform, ${userName}!`)
.setHtmlBody(`
Welcome ${userName}!
Thank you for joining us.
`)
.setPriority('normal')
.build();
// Send email using email service
await this.sendEmail(email);
}
private async sendEmail(message: EmailMessage): Promise {
// Implementation to send email via SMTP/API
}
}
// === Another Builder Example: Query Builder ===
class SqlQueryBuilder {
private table = '';
private selects: string[] = ['*'];
private wheres: Array<{ column: string; operator: string; value: any }> = [];
private joins: Array<{ type: string; table: string; on: string }> = [];
private orderBy?: { column: string; direction: 'ASC' | 'DESC' };
private limit?: number;
private offset?: number;
from(table: string): this {
if (!table || table.trim().length === 0) {
throw new Error('Table name cannot be empty');
}
this.table = table;
return this;
}
select(columns: string[]): this {
this.selects = columns;
return this;
}
where(column: string, operator: string, value: any): this {
this.wheres.push({ column, operator, value });
return this;
}
join(type: 'INNER' | 'LEFT' | 'RIGHT', table: string, on: string): this {
this.joins.push({ type, table, on });
return this;
}
orderBy(column: string, direction: 'ASC' | 'DESC' = 'ASC'): this {
this.orderBy = { column, direction };
return this;
}
limit(limit: number): this {
if (limit <= 0) {
throw new Error('Limit must be greater than 0');
}
this.limit = limit;
return this;
}
offset(offset: number): this {
if (offset < 0) {
throw new Error('Offset must be greater than or equal to 0');
}
this.offset = offset;
return this;
}
build(): { sql: string; params: any[] } {
if (!this.table) {
throw new Error('Table name is required');
}
const params: any[] = [];
let sql = `SELECT ${this.selects.join(', ')} FROM ${this.table}`;
// JOINs
this.joins.forEach(join => {
sql += ` ${join.type} JOIN ${join.table} ON ${join.on}`;
});
// WHERE clauses
if (this.wheres.length > 0) {
const whereConditions = this.wheres.map((where, index) => {
params.push(where.value);
return `${where.column} ${where.operator} $${params.length}`;
});
sql += ` WHERE ${whereConditions.join(' AND ')}`;
}
// ORDER BY
if (this.orderBy) {
sql += ` ORDER BY ${this.orderBy.column} ${this.orderBy.direction}`;
}
// LIMIT and OFFSET
if (this.limit) {
sql += ` LIMIT $${params.length + 1}`;
params.push(this.limit);
}
if (this.offset) {
sql += ` OFFSET $${params.length + 1}`;
params.push(this.offset);
}
return { sql, params };
}
}
// Usage
const query = new SqlQueryBuilder()
.from('users')
.select(['id', 'name', 'email'])
.where('status', '=', 'active')
.where('created_at', '>', new Date('2024-01-01'))
.join('LEFT', 'orders', 'users.id = orders.user_id')
.orderBy('created_at', 'DESC')
.limit(10)
.offset(0)
.build();
console.log(query.sql); // SELECT id, name, email FROM users LEFT JOIN orders ON users.id = orders.user_id WHERE status = $1 AND created_at > $2 ORDER BY created_at DESC LIMIT $3 OFFSET $4
5) Prototype (Creational)
Create new objects by cloning existing ones.
Example
// === Node.js TypeScript ===
interface NotificationTemplate {
id: string;
type: 'email' | 'sms' | 'push';
subject?: string;
body: string;
variables: Record;
metadata: {
priority: number;
retryCount: number;
tags: string[];
createdAt: Date;
};
}
class NotificationTemplate implements NotificationTemplate {
id: string;
type: 'email' | 'sms' | 'push';
subject?: string;
body: string;
variables: Record;
metadata: {
priority: number;
retryCount: number;
tags: string[];
createdAt: Date;
};
constructor(config: Partial & { type: 'email' | 'sms' | 'push'; body: string }) {
this.id = config.id || `template-${Date.now()}`;
this.type = config.type;
this.subject = config.subject;
this.body = config.body;
this.variables = config.variables || {};
this.metadata = {
priority: config.metadata?.priority || 0,
retryCount: config.metadata?.retryCount || 0,
tags: config.metadata?.tags || [],
createdAt: config.metadata?.createdAt || new Date()
};
}
// Prototype method - deep clone the template
clone(): NotificationTemplate {
// Deep clone including nested objects and dates
const cloned = Object.create(Object.getPrototypeOf(this));
cloned.id = `${this.id}-clone-${Date.now()}`;
cloned.type = this.type;
cloned.subject = this.subject;
cloned.body = this.body;
cloned.variables = { ...this.variables };
cloned.metadata = {
priority: this.metadata.priority,
retryCount: 0, // Reset retry count for new clone
tags: [...this.metadata.tags],
createdAt: new Date() // New creation date
};
return cloned;
}
// Create a customized version from the prototype
customize(customizations: Partial): NotificationTemplate {
const customized = this.clone();
if (customizations.subject !== undefined) customized.subject = customizations.subject;
if (customizations.body !== undefined) customized.body = customizations.body;
if (customizations.variables) {
customized.variables = { ...customized.variables, ...customizations.variables };
}
if (customizations.metadata) {
customized.metadata = { ...customized.metadata, ...customizations.metadata };
}
return customized;
}
// Render template with variables
render(vars?: Record): string {
let rendered = this.body;
const allVars = { ...this.variables, ...vars };
Object.entries(allVars).forEach(([key, value]) => {
rendered = rendered.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value);
});
return rendered;
}
}
// Usage: Create base template (expensive configuration)
const baseEmailTemplate = new NotificationTemplate({
type: 'email',
subject: 'Order Update',
body: 'Hello {{name}}, your order #{{orderId}} is now {{status}}.',
variables: { company: 'MyShop', supportEmail: 'support@myshop.com' },
metadata: {
priority: 1,
retryCount: 0,
tags: ['order', 'notification'],
createdAt: new Date()
}
});
// Clone and customize for different scenarios (cheap operation)
const shippingNotification = baseEmailTemplate.customize({
subject: 'Your Order Has Shipped!',
variables: { status: 'shipped' }
});
const deliveredNotification = baseEmailTemplate.customize({
subject: 'Order Delivered',
variables: { status: 'delivered' }
});
// Use the cloned templates
console.log(shippingNotification.render({ name: 'John', orderId: '12345' }));
// Output: Hello John, your order #12345 is now shipped.
// Registry pattern with prototypes
class TemplateRegistry {
private templates = new Map();
register(name: string, template: NotificationTemplate): void {
this.templates.set(name, template);
}
getPrototype(name: string): NotificationTemplate | undefined {
return this.templates.get(name);
}
createFromPrototype(name: string, customizations?: Partial): NotificationTemplate {
const prototype = this.templates.get(name);
if (!prototype) {
throw new Error(`Template prototype "${name}" not found`);
}
return customizations ? prototype.customize(customizations) : prototype.clone();
}
}
// Setup registry with prototypes
const registry = new TemplateRegistry();
registry.register('order-email', baseEmailTemplate);
registry.register('welcome-email', new NotificationTemplate({
type: 'email',
subject: 'Welcome to MyShop!',
body: 'Welcome {{name}}! Get {{discount}}% off your first order.',
metadata: { priority: 2, retryCount: 0, tags: ['welcome'], createdAt: new Date() }
}));
// Create instances from prototypes
const order1 = registry.createFromPrototype('order-email', {
variables: { orderId: '12345', status: 'processing' }
});
const order2 = registry.createFromPrototype('order-email', {
variables: { orderId: '67890', status: 'shipped' }
});
// === NestJS ===
import { Injectable } from '@nestjs/common';
@Injectable()
export class NotificationTemplate {
// Same implementation as above
}
@Injectable()
export class TemplateRegistry {
// Same implementation as above
}
@Injectable()
export class NotificationService {
constructor(private templateRegistry: TemplateRegistry) {}
async sendOrderNotification(orderId: string, userId: string, status: string): Promise {
const template = this.templateRegistry.createFromPrototype('order-email', {
variables: { orderId, status }
});
const user = await this.getUser(userId);
const rendered = template.render({ name: user.name, orderId, status });
// Send notification
await this.sendEmail(user.email, template.subject || '', rendered);
}
private async getUser(userId: string): Promise<{ name: string; email: string }> {
// Fetch user from database
return { name: 'John Doe', email: 'john@example.com' };
}
private async sendEmail(to: string, subject: string, body: string): Promise {
// Send email implementation
}
}
6) Adapter (Structural)
Convert one interface into another clients expect.
Example
// === Node.js TypeScript ===
interface FileUploadResult {
url: string;
key: string;
size: number;
contentType: string;
uploadedAt: Date;
}
interface IFileStorage {
upload(file: Buffer, filename: string, options?: { contentType?: string; folder?: string }): Promise;
delete(key: string): Promise;
getUrl(key: string, expiresIn?: number): Promise;
exists(key: string): Promise;
}
// Legacy AWS S3 SDK v2 (different API)
import { S3 as LegacyS3 } from 'aws-sdk';
class LegacyS3Service {
private s3: LegacyS3;
constructor() {
this.s3 = new LegacyS3({
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
region: process.env.AWS_REGION || 'us-east-1'
});
}
// Old API: different method names and return format
putObject(params: {
Bucket: string;
Key: string;
Body: Buffer;
ContentType?: string;
}): Promise<{ Location: string; ETag: string }> {
return new Promise((resolve, reject) => {
this.s3.putObject(params, (err, data) => {
if (err) reject(err);
else resolve({
Location: `https://${params.Bucket}.s3.amazonaws.com/${params.Key}`,
ETag: data.ETag || ''
});
});
});
}
deleteObject(params: { Bucket: string; Key: string }): Promise {
return new Promise((resolve, reject) => {
this.s3.deleteObject(params, (err) => {
if (err) reject(err);
else resolve();
});
});
}
getSignedUrl(params: { Bucket: string; Key: string; Expires: number }): Promise {
return new Promise((resolve, reject) => {
this.s3.getSignedUrl('getObject', params, (err, url) => {
if (err) reject(err);
else resolve(url);
});
});
}
}
// Adapter: Wraps legacy S3 to match our IFileStorage interface
class S3StorageAdapter implements IFileStorage {
private bucket: string;
private legacyS3: LegacyS3Service;
constructor(bucket: string, legacyS3?: LegacyS3Service) {
this.bucket = bucket;
this.legacyS3 = legacyS3 || new LegacyS3Service();
}
async upload(
file: Buffer,
filename: string,
options?: { contentType?: string; folder?: string }
): Promise {
try {
const key = options?.folder ? `${options.folder}/${filename}` : filename;
const result = await this.legacyS3.putObject({
Bucket: this.bucket,
Key: key,
Body: file,
ContentType: options?.contentType || 'application/octet-stream'
});
return {
url: result.Location,
key: key,
size: file.length,
contentType: options?.contentType || 'application/octet-stream',
uploadedAt: new Date()
};
} catch (error) {
throw new Error(`Failed to upload file: ${error.message}`);
}
}
async delete(key: string): Promise {
try {
await this.legacyS3.deleteObject({
Bucket: this.bucket,
Key: key
});
} catch (error) {
throw new Error(`Failed to delete file: ${error.message}`);
}
}
async getUrl(key: string, expiresIn: number = 3600): Promise {
try {
return await this.legacyS3.getSignedUrl({
Bucket: this.bucket,
Key: key,
Expires: expiresIn
});
} catch (error) {
throw new Error(`Failed to get file URL: ${error.message}`);
}
}
async exists(key: string): Promise {
// Legacy S3 doesn't have direct exists check, would need headObject
// This is simplified
try {
await this.getUrl(key, 1);
return true;
} catch {
return false;
}
}
}
// Another adapter for local filesystem (for testing/development)
import * as fs from 'fs/promises';
import * as path from 'path';
class LocalStorageAdapter implements IFileStorage {
private baseDir: string;
constructor(baseDir: string = './uploads') {
this.baseDir = baseDir;
}
async upload(
file: Buffer,
filename: string,
options?: { contentType?: string; folder?: string }
): Promise {
try {
const dir = options?.folder ? path.join(this.baseDir, options.folder) : this.baseDir;
await fs.mkdir(dir, { recursive: true });
const filePath = path.join(dir, filename);
await fs.writeFile(filePath, file);
return {
url: `/uploads/${options?.folder ? options.folder + '/' : ''}${filename}`,
key: filePath,
size: file.length,
contentType: options?.contentType || 'application/octet-stream',
uploadedAt: new Date()
};
} catch (error) {
throw new Error(`Failed to upload file locally: ${error.message}`);
}
}
async delete(key: string): Promise {
try {
await fs.unlink(key);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
throw new Error(`Failed to delete file: ${error.message}`);
}
}
}
async getUrl(key: string): Promise {
return `/files/${path.relative(this.baseDir, key)}`;
}
async exists(key: string): Promise {
try {
await fs.access(key);
return true;
} catch {
return false;
}
}
}
// Usage: Client code doesn't need to know which storage implementation
class FileUploadService {
constructor(private storage: IFileStorage) {}
async uploadAvatar(userId: string, imageBuffer: Buffer, filename: string): Promise {
const validImageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
const contentType = this.detectContentType(filename);
if (!validImageTypes.includes(contentType)) {
throw new Error('Invalid image type. Allowed: JPEG, PNG, GIF, WebP');
}
if (imageBuffer.length > 5 * 1024 * 1024) { // 5MB limit
throw new Error('File size exceeds 5MB limit');
}
return await this.storage.upload(imageBuffer, filename, {
contentType,
folder: `avatars/${userId}`
});
}
private detectContentType(filename: string): string {
const ext = path.extname(filename).toLowerCase();
const types: Record = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.webp': 'image/webp'
};
return types[ext] || 'application/octet-stream';
}
}
// Switch implementations easily
const storage = process.env.NODE_ENV === 'production'
? new S3StorageAdapter(process.env.S3_BUCKET || 'my-bucket')
: new LocalStorageAdapter('./local-uploads');
const uploadService = new FileUploadService(storage);
// === NestJS ===
import { Injectable, Inject } from '@nestjs/common';
@Injectable()
export class S3StorageAdapter implements IFileStorage {
// Same implementation as above
}
@Injectable()
export class LocalStorageAdapter implements IFileStorage {
// Same implementation as above
}
const FILE_STORAGE_PROVIDER = {
provide: 'FILE_STORAGE',
useFactory: (): IFileStorage => {
return process.env.NODE_ENV === 'production'
? new S3StorageAdapter(process.env.S3_BUCKET)
: new LocalStorageAdapter('./local-uploads');
}
};
@Injectable()
export class FileUploadService {
constructor(@Inject('FILE_STORAGE') private storage: IFileStorage) {
// Same implementation as above
}
}
7) Facade (Structural)
Provide a simple interface to a complex subsystem.
Example
// === Node.js TypeScript ===
// Complex subsystems - typically would be in separate modules
class AuthenticationService {
async verifyToken(token: string): Promise<{ userId: string; email: string } | null> {
// JWT verification logic
try {
const decoded = this.decodeJWT(token);
if (!decoded || decoded.exp < Date.now() / 1000) {
return null;
}
return { userId: decoded.userId, email: decoded.email };
} catch {
return null;
}
}
async checkPermission(userId: string, resource: string, action: string): Promise {
// Complex permission checking logic
return true; // Simplified
}
private decodeJWT(token: string): any {
// JWT decoding implementation
return { userId: 'u1', email: 'user@example.com', exp: Date.now() / 1000 + 3600 };
}
}
class UserRepository {
async findById(id: string): Promise {
// Database query
return { id, email: 'user@example.com', name: 'John Doe', createdAt: new Date() };
}
async updateLastLogin(id: string): Promise {
// Update last login timestamp
}
}
class OrderRepository {
async findByUserId(userId: string): Promise {
// Database query with joins
return [
{ id: 'o1', userId, total: 99.99, status: 'completed', createdAt: new Date() },
{ id: 'o2', userId, total: 149.99, status: 'pending', createdAt: new Date() }
];
}
async getTotalSpent(userId: string): Promise {
// Aggregate query
return 249.98;
}
}
class PaymentService {
async getPaymentMethods(userId: string): Promise {
// External API call or database query
return [
{ id: 'pm1', type: 'card', last4: '4242', brand: 'visa' }
];
}
async getTransactions(userId: string, limit: number = 10): Promise {
// Complex query with pagination
return [];
}
}
class NotificationService {
async getUnreadCount(userId: string): Promise {
// Database count query
return 3;
}
async getRecent(userId: string, limit: number = 5): Promise {
// Query with sorting and limit
return [];
}
}
// Facade: Simplifies complex operations across multiple services
class UserAccountFacade {
constructor(
private authService = new AuthenticationService(),
private userRepo = new UserRepository(),
private orderRepo = new OrderRepository(),
private paymentService = new PaymentService(),
private notificationService = new NotificationService()
) {}
// Single method that orchestrates multiple service calls
async getAccountDashboard(token: string): Promise<{
user: any;
orders: any[];
stats: { totalSpent: number; orderCount: number; unreadNotifications: number };
paymentMethods: any[];
}> {
// Verify authentication
const auth = await this.authService.verifyToken(token);
if (!auth) {
throw new Error('Invalid or expired token');
}
// Check permissions
const hasAccess = await this.authService.checkPermission(auth.userId, 'account', 'read');
if (!hasAccess) {
throw new Error('Insufficient permissions');
}
// Update last login
await this.userRepo.updateLastLogin(auth.userId);
// Fetch all required data in parallel
const [user, orders, totalSpent, paymentMethods, unreadCount] = await Promise.all([
this.userRepo.findById(auth.userId),
this.orderRepo.findByUserId(auth.userId),
this.orderRepo.getTotalSpent(auth.userId),
this.paymentService.getPaymentMethods(auth.userId),
this.notificationService.getUnreadCount(auth.userId)
]);
return {
user,
orders,
stats: {
totalSpent,
orderCount: orders.length,
unreadNotifications: unreadCount
},
paymentMethods
};
}
// Another simplified operation
async completeProfileSetup(userId: string, profileData: any): Promise {
// Complex orchestration: validate, update user, send welcome email, create audit log
await this.userRepo.update(userId, profileData);
// Additional operations would go here
}
}
// Usage: Client code only needs to know about the facade
const facade = new UserAccountFacade();
try {
const dashboard = await facade.getAccountDashboard('bearer-token-here');
console.log(dashboard);
} catch (error) {
console.error('Failed to load dashboard:', error.message);
}
// === NestJS ===
import { Injectable, UnauthorizedException, ForbiddenException } from '@nestjs/common';
@Injectable()
export class AuthenticationService {
// Same implementation
}
@Injectable()
export class UserRepository {
// Same implementation
}
@Injectable()
export class OrderRepository {
// Same implementation
}
@Injectable()
export class PaymentService {
// Same implementation
}
@Injectable()
export class NotificationService {
// Same implementation
}
@Injectable()
export class UserAccountFacade {
constructor(
private authService: AuthenticationService,
private userRepo: UserRepository,
private orderRepo: OrderRepository,
private paymentService: PaymentService,
private notificationService: NotificationService
) {}
async getAccountDashboard(token: string) {
const auth = await this.authService.verifyToken(token);
if (!auth) {
throw new UnauthorizedException('Invalid or expired token');
}
const hasAccess = await this.authService.checkPermission(auth.userId, 'account', 'read');
if (!hasAccess) {
throw new ForbiddenException('Insufficient permissions');
}
// Rest of implementation same as above
const [user, orders, totalSpent, paymentMethods, unreadCount] = await Promise.all([
this.userRepo.findById(auth.userId),
this.orderRepo.findByUserId(auth.userId),
this.orderRepo.getTotalSpent(auth.userId),
this.paymentService.getPaymentMethods(auth.userId),
this.notificationService.getUnreadCount(auth.userId)
]);
return {
user,
orders,
stats: {
totalSpent,
orderCount: orders.length,
unreadNotifications: unreadCount
},
paymentMethods
};
}
}
@Controller('account')
export class AccountController {
constructor(private accountFacade: UserAccountFacade) {}
@Get('dashboard')
async getDashboard(@Headers('authorization') token: string) {
return this.accountFacade.getAccountDashboard(token);
}
}
8) Proxy (Structural)
Control access to another object (cache, lazy, auth).
Example
// === Node.js TypeScript ===
interface CacheEntry {
data: T;
expiresAt: number;
}
class CacheProxy {
private cache = new Map>();
private defaultTTL: number;
constructor(defaultTTLSeconds: number = 300) {
this.defaultTTL = defaultTTLSeconds * 1000;
}
private generateKey(args: any[]): string {
return JSON.stringify(args);
}
private isExpired(entry: CacheEntry): boolean {
return Date.now() > entry.expiresAt;
}
private cleanup(): void {
for (const [key, entry] of this.cache.entries()) {
if (this.isExpired(entry)) {
this.cache.delete(key);
}
}
}
wrap(
fn: (...args: P) => Promise,
ttlSeconds?: number
): (...args: P) => Promise {
const ttl = (ttlSeconds || this.defaultTTL / 1000) * 1000;
return async (...args: P): Promise => {
// Periodic cleanup
if (Math.random() < 0.01) { // ~1% of requests trigger cleanup
this.cleanup();
}
const key = this.generateKey(args);
const cached = this.cache.get(key);
if (cached && !this.isExpired(cached)) {
return cached.data as R;
}
try {
const result = await fn(...args);
this.cache.set(key, {
data: result as any,
expiresAt: Date.now() + ttl
});
return result;
} catch (error) {
// Don't cache errors
throw error;
}
};
}
clear(): void {
this.cache.clear();
}
invalidate(keyPattern?: string): void {
if (!keyPattern) {
this.cache.clear();
return;
}
for (const key of this.cache.keys()) {
if (key.includes(keyPattern)) {
this.cache.delete(key);
}
}
}
}
// Usage examples
class UserService {
private cacheProxy = new CacheProxy(300); // 5 minute default TTL
// Expensive database query - cached for 5 minutes
async getUserById(id: string): Promise {
const fetchUser = async (userId: string) => {
console.log(`Fetching user ${userId} from database...`);
// Simulate database query
await new Promise(resolve => setTimeout(resolve, 100));
return {
id: userId,
name: 'John Doe',
email: 'john@example.com',
profile: { bio: '...', avatar: '...' }
};
};
const cachedFetch = this.cacheProxy.wrap(fetchUser, 300);
return cachedFetch(id);
}
// Expensive calculation - cached for 1 hour
async getUserStats(userId: string): Promise {
const calculateStats = async (id: string) => {
console.log(`Calculating stats for user ${id}...`);
// Expensive calculations, aggregations
await new Promise(resolve => setTimeout(resolve, 500));
return {
totalOrders: 42,
totalSpent: 1250.99,
averageOrderValue: 29.79
};
};
const cachedCalc = this.cacheProxy.wrap(calculateStats, 3600);
return cachedCalc(userId);
}
// When user updates, invalidate cache
async updateUser(userId: string, data: any): Promise {
// Update in database
await this.saveUser(userId, data);
// Invalidate all cached data for this user
this.cacheProxy.invalidate(userId);
}
private async saveUser(userId: string, data: any): Promise {
// Database update
}
}
// Lazy loading proxy
class LazyDataProxy {
private data: T | null = null;
private loader: () => Promise;
private loading: Promise | null = null;
constructor(loader: () => Promise) {
this.loader = loader;
}
async get(): Promise {
if (this.data !== null) {
return this.data;
}
if (this.loading) {
return this.loading;
}
this.loading = this.loader().then(result => {
this.data = result;
this.loading = null;
return result;
});
return this.loading;
}
invalidate(): void {
this.data = null;
this.loading = null;
}
}
// Protection proxy - access control
class ProtectedUserService {
private userService = new UserService();
async getUserById(userId: string, requesterId: string, requesterRole: string): Promise {
// Authorization check
if (requesterRole !== 'admin' && requesterId !== userId) {
throw new Error('Access denied: You can only view your own profile');
}
return this.userService.getUserById(userId);
}
async deleteUser(userId: string, requesterRole: string): Promise {
// Permission check
if (requesterRole !== 'admin') {
throw new Error('Access denied: Only admins can delete users');
}
// Perform deletion
await this.userService.deleteUser(userId);
}
}
// === NestJS ===
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, CACHE_MANAGER, Inject } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Cache } from 'cache-manager';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
async intercept(context: ExecutionContext, next: CallHandler): Promise> {
const request = context.switchToHttp().getRequest();
const { method, url, params, query, body } = request;
// Skip caching for non-GET requests
if (method !== 'GET') {
return next.handle();
}
// Generate cache key
const cacheKey = `cache:${method}:${url}:${JSON.stringify({ params, query })}`;
// Try to get from cache
const cached = await this.cacheManager.get(cacheKey);
if (cached) {
return of(cached);
}
// Execute and cache the result
return next.handle().pipe(
tap(async (data) => {
// Cache for 5 minutes
await this.cacheManager.set(cacheKey, data, { ttl: 300 });
})
);
}
}
// Usage in controller
@Controller('users')
@UseInterceptors(CacheInterceptor)
export class UsersController {
constructor(private userService: UserService) {}
@Get(':id')
async getUser(@Param('id') id: string) {
return this.userService.getUserById(id);
}
}
// Logging proxy
@Injectable()
export class LoggingProxy implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable {
const request = context.switchToHttp().getRequest();
const startTime = Date.now();
const { method, url } = request;
console.log(`[${new Date().toISOString()}] ${method} ${url} - Started`);
return next.handle().pipe(
tap({
next: () => {
const duration = Date.now() - startTime;
console.log(`[${new Date().toISOString()}] ${method} ${url} - Completed in ${duration}ms`);
},
error: (error) => {
const duration = Date.now() - startTime;
console.error(`[${new Date().toISOString()}] ${method} ${url} - Failed after ${duration}ms:`, error.message);
}
})
);
}
}
9) Decorator (Structural)
Add responsibilities to objects dynamically without subclassing.
Example
// === Node.js TypeScript ===
import { performance } from 'perf_hooks';
// Timing decorator with detailed metrics
function LogExecutionTime(operation?: string) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
const methodName = operation || propertyKey;
descriptor.value = async function(...args: any[]) {
const start = performance.now();
const startMemory = process.memoryUsage().heapUsed;
try {
const result = await originalMethod.apply(this, args);
const end = performance.now();
const endMemory = process.memoryUsage().heapUsed;
const duration = (end - start).toFixed(2);
const memoryDelta = ((endMemory - startMemory) / 1024 / 1024).toFixed(2);
console.log(`[${methodName}] Completed in ${duration}ms (Memory: ${memoryDelta}MB)`);
return result;
} catch (error) {
const end = performance.now();
const duration = (end - start).toFixed(2);
console.error(`[${methodName}] Failed after ${duration}ms:`, error.message);
throw error;
}
};
return descriptor;
};
}
// Retry decorator
function Retry(maxAttempts: number = 3, delayMs: number = 1000) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args: any[]) {
let lastError: Error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
lastError = error as Error;
if (attempt < maxAttempts) {
console.warn(`[${propertyKey}] Attempt ${attempt} failed, retrying in ${delayMs}ms...`);
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
}
throw new Error(`${propertyKey} failed after ${maxAttempts} attempts: ${lastError!.message}`);
};
return descriptor;
};
}
// Cache decorator
function CacheResult(ttlSeconds: number = 300) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
const cache = new Map();
descriptor.value = async function(...args: any[]) {
const cacheKey = `${propertyKey}:${JSON.stringify(args)}`;
const cached = cache.get(cacheKey);
if (cached && cached.expiresAt > Date.now()) {
return cached.data;
}
const result = await originalMethod.apply(this, args);
cache.set(cacheKey, {
data: result,
expiresAt: Date.now() + (ttlSeconds * 1000)
});
return result;
};
return descriptor;
};
}
// Validate decorator
function Validate(...validators: Array<(value: any) => boolean | string>) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args: any[]) {
for (const validator of validators) {
const result = validator(args[0]);
if (result !== true) {
const message = typeof result === 'string' ? result : 'Validation failed';
throw new Error(`[${propertyKey}] ${message}`);
}
}
return originalMethod.apply(this, args);
};
return descriptor;
};
}
// Rate limit decorator
function RateLimit(maxCalls: number, windowMs: number) {
const callHistory = new Map();
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args: any[]) {
const key = `${target.constructor.name}:${propertyKey}`;
const now = Date.now();
const calls = callHistory.get(key) || [];
// Remove old calls outside the window
const recentCalls = calls.filter(time => now - time < windowMs);
if (recentCalls.length >= maxCalls) {
throw new Error(`Rate limit exceeded for ${propertyKey}. Max ${maxCalls} calls per ${windowMs}ms`);
}
recentCalls.push(now);
callHistory.set(key, recentCalls);
return originalMethod.apply(this, args);
};
return descriptor;
};
}
// Usage: Combine multiple decorators
class PaymentService {
@LogExecutionTime('Process Payment')
@Retry(3, 1000)
@RateLimit(10, 60000) // Max 10 calls per minute
@Validate(
(data: any) => data.amount > 0 || 'Amount must be positive',
(data: any) => data.currency?.length === 3 || 'Invalid currency code'
)
async processPayment(data: { amount: number; currency: string; userId: string }): Promise {
// Simulate API call that might fail
if (Math.random() < 0.3) {
throw new Error('Payment gateway temporarily unavailable');
}
await new Promise(resolve => setTimeout(resolve, 100));
return {
transactionId: `txn_${Date.now()}`,
status: 'completed',
amount: data.amount,
currency: data.currency
};
}
@CacheResult(600) // Cache for 10 minutes
@LogExecutionTime('Get User Balance')
async getUserBalance(userId: string): Promise {
// Expensive database query
await new Promise(resolve => setTimeout(resolve, 200));
return 1250.99;
}
}
// === NestJS ===
import { Injectable, UseInterceptors, applyDecorators, SetMetadata } from '@nestjs/common';
import { CacheInterceptor, CacheKey, CacheTTL } from '@nestjs/cache-manager';
// Custom decorator combining multiple interceptors
export function CachedApi(ttl: number = 300) {
return applyDecorators(
UseInterceptors(CacheInterceptor),
CacheTTL(ttl),
SetMetadata('cached', true)
);
}
export function RateLimited(maxCalls: number, windowMs: number) {
return SetMetadata('rateLimit', { maxCalls, windowMs });
}
@Injectable()
export class PaymentService {
@CachedApi(600)
async getUserBalance(userId: string): Promise {
// Implementation
return 1250.99;
}
@RateLimited(10, 60000)
async processPayment(data: any): Promise {
// Implementation
return { transactionId: 'txn_123' };
}
}
// Guard for rate limiting
import { Injectable, CanActivate, ExecutionContext, HttpException, HttpStatus } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RateLimitGuard implements CanActivate {
private requests = new Map();
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const rateLimit = this.reflector.get<{ maxCalls: number; windowMs: number }>('rateLimit', context.getHandler());
if (!rateLimit) {
return true;
}
const request = context.switchToHttp().getRequest();
const key = `${request.ip}:${context.getHandler().name}`;
const now = Date.now();
const calls = this.requests.get(key) || [];
const recentCalls = calls.filter(time => now - time < rateLimit.windowMs);
if (recentCalls.length >= rateLimit.maxCalls) {
throw new HttpException('Rate limit exceeded', HttpStatus.TOO_MANY_REQUESTS);
}
recentCalls.push(now);
this.requests.set(key, recentCalls);
return true;
}
}
10) Composite (Structural)
Treat individual objects and compositions uniformly.
Example
// === Node.js TypeScript ===
// File system structure using Composite pattern
interface FileSystemNode {
getName(): string;
getSize(): number;
getType(): 'file' | 'directory';
getPath(): string;
find(pattern: string): FileSystemNode[];
accept(visitor: FileSystemVisitor): void;
}
interface FileSystemVisitor {
visitFile(file: File): void;
visitDirectory(directory: Directory): void;
}
class File implements FileSystemNode {
constructor(
private name: string,
private size: number,
private parent: Directory | null = null
) {}
getName(): string {
return this.name;
}
getSize(): number {
return this.size;
}
getType(): 'file' | 'directory' {
return 'file';
}
getPath(): string {
return this.parent ? `${this.parent.getPath()}/${this.name}` : this.name;
}
find(pattern: string): FileSystemNode[] {
return this.name.includes(pattern) ? [this] : [];
}
accept(visitor: FileSystemVisitor): void {
visitor.visitFile(this);
}
}
class Directory implements FileSystemNode {
private children: FileSystemNode[] = [];
constructor(
private name: string,
private parent: Directory | null = null
) {}
getName(): string {
return this.name;
}
getSize(): number {
// Composite: sum of all children
return this.children.reduce((total, child) => total + child.getSize(), 0);
}
getType(): 'file' | 'directory' {
return 'directory';
}
getPath(): string {
return this.parent ? `${this.parent.getPath()}/${this.name}` : this.name;
}
add(child: FileSystemNode): void {
this.children.push(child);
}
remove(child: FileSystemNode): void {
const index = this.children.indexOf(child);
if (index > -1) {
this.children.splice(index, 1);
}
}
getChildren(): FileSystemNode[] {
return [...this.children];
}
find(pattern: string): FileSystemNode[] {
const results: FileSystemNode[] = [];
// Search in current directory name
if (this.name.includes(pattern)) {
results.push(this);
}
// Recursively search in children
for (const child of this.children) {
results.push(...child.find(pattern));
}
return results;
}
accept(visitor: FileSystemVisitor): void {
visitor.visitDirectory(this);
// Visit all children
for (const child of this.children) {
child.accept(visitor);
}
}
}
// Visitor pattern integration
class FileSizeCalculator implements FileSystemVisitor {
private totalSize = 0;
private fileCount = 0;
private directoryCount = 0;
visitFile(file: File): void {
this.totalSize += file.getSize();
this.fileCount++;
}
visitDirectory(directory: Directory): void {
this.directoryCount++;
}
getStats(): { totalSize: number; fileCount: number; directoryCount: number } {
return {
totalSize: this.totalSize,
fileCount: this.fileCount,
directoryCount: this.directoryCount
};
}
}
// Usage
const root = new Directory('root');
const documents = new Directory('documents', root);
const images = new Directory('images', root);
const file1 = new File('readme.txt', 1024, root);
const file2 = new File('report.pdf', 5120, documents);
const file3 = new File('photo.jpg', 2048, images);
const file4 = new File('photo2.jpg', 3072, images);
root.add(file1);
root.add(documents);
root.add(images);
documents.add(file2);
images.add(file3);
images.add(file4);
console.log(`Total size: ${root.getSize()} bytes`); // 11264 bytes
console.log(`Images folder size: ${images.getSize()} bytes`); // 5120 bytes
// Find operation works on both files and directories
const results = root.find('photo');
console.log(`Found ${results.length} items matching 'photo'`);
// Visitor pattern
const calculator = new FileSizeCalculator();
root.accept(calculator);
const stats = calculator.getStats();
console.log('Stats:', stats);
// === Another Example: Permission System ===
interface PermissionNode {
hasPermission(permission: string): boolean;
addPermission(permission: string): void;
removePermission(permission: string): void;
getAllPermissions(): string[];
}
class Permission implements PermissionNode {
private permissions: Set = new Set();
hasPermission(permission: string): boolean {
return this.permissions.has(permission);
}
addPermission(permission: string): void {
this.permissions.add(permission);
}
removePermission(permission: string): void {
this.permissions.delete(permission);
}
getAllPermissions(): string[] {
return Array.from(this.permissions);
}
}
class PermissionGroup implements PermissionNode {
private permissions: Set = new Set();
private children: PermissionNode[] = [];
constructor(private name: string) {}
add(node: PermissionNode): void {
this.children.push(node);
}
remove(node: PermissionNode): void {
const index = this.children.indexOf(node);
if (index > -1) {
this.children.splice(index, 1);
}
}
hasPermission(permission: string): boolean {
// Check own permissions first
if (this.permissions.has(permission)) {
return true;
}
// Check children recursively
return this.children.some(child => child.hasPermission(permission));
}
addPermission(permission: string): void {
this.permissions.add(permission);
}
removePermission(permission: string): void {
this.permissions.delete(permission);
// Optionally remove from children
this.children.forEach(child => child.removePermission(permission));
}
getAllPermissions(): string[] {
const ownPerms = Array.from(this.permissions);
const childPerms = this.children.flatMap(child => child.getAllPermissions());
// Merge and deduplicate
return Array.from(new Set([...ownPerms, ...childPerms]));
}
}
// Usage
const adminGroup = new PermissionGroup('Admin');
adminGroup.addPermission('users.read');
adminGroup.addPermission('users.write');
const userPermissions = new Permission();
userPermissions.addPermission('profile.read');
userPermissions.addPermission('profile.write');
adminGroup.add(userPermissions);
console.log(adminGroup.hasPermission('users.write')); // true
console.log(adminGroup.hasPermission('profile.read')); // true (from child)
console.log(adminGroup.getAllPermissions()); // ['users.read', 'users.write', 'profile.read', 'profile.write']
// === NestJS ===
import { Injectable } from '@nestjs/common';
@Injectable()
export class FileSystemService {
createDirectory(name: string, parent?: Directory): Directory {
return new Directory(name, parent || null);
}
createFile(name: string, size: number, parent: Directory): File {
const file = new File(name, size, parent);
parent.add(file);
return file;
}
calculateDirectorySize(directory: Directory): number {
return directory.getSize();
}
}
11) Strategy (Behavioral)
Define a family of algorithms; make them interchangeable.
Example
// === Node.js TypeScript ===
interface PaymentResult {
success: boolean;
transactionId?: string;
error?: string;
gateway?: string;
}
interface PaymentStrategy {
processPayment(amount: number, currency: string, metadata: Record): Promise;
refund(transactionId: string, amount: number): Promise;
validate(amount: number, metadata: Record): boolean;
}
// Credit Card Strategy
class CreditCardStrategy implements PaymentStrategy {
constructor(
private apiKey: string,
private merchantId: string
) {}
async processPayment(amount: number, currency: string, metadata: Record): Promise {
try {
if (!this.validate(amount, metadata)) {
return { success: false, error: 'Invalid payment data', gateway: 'creditcard' };
}
// Simulate API call to payment gateway
const response = await this.callPaymentGateway({
amount,
currency,
cardNumber: metadata.cardNumber,
cvv: metadata.cvv,
expiryDate: metadata.expiryDate,
cardholderName: metadata.cardholderName
});
return {
success: response.status === 'approved',
transactionId: response.transactionId,
gateway: 'creditcard'
};
} catch (error) {
return {
success: false,
error: error.message,
gateway: 'creditcard'
};
}
}
async refund(transactionId: string, amount: number): Promise {
try {
const response = await this.callRefundGateway(transactionId, amount);
return {
success: response.status === 'refunded',
transactionId: response.refundId,
gateway: 'creditcard'
};
} catch (error) {
return {
success: false,
error: error.message,
gateway: 'creditcard'
};
}
}
validate(amount: number, metadata: Record): boolean {
if (amount <= 0) return false;
if (!metadata.cardNumber || !this.isValidCardNumber(metadata.cardNumber)) return false;
if (!metadata.expiryDate || !this.isValidExpiryDate(metadata.expiryDate)) return false;
if (!metadata.cvv || metadata.cvv.length !== 3) return false;
return true;
}
private async callPaymentGateway(data: any): Promise {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 100));
return {
status: 'approved',
transactionId: `txn_cc_${Date.now()}`
};
}
private async callRefundGateway(transactionId: string, amount: number): Promise {
await new Promise(resolve => setTimeout(resolve, 100));
return {
status: 'refunded',
refundId: `refund_${Date.now()}`
};
}
private isValidCardNumber(cardNumber: string): boolean {
return /^\d{13,19}$/.test(cardNumber.replace(/\s/g, ''));
}
private isValidExpiryDate(expiryDate: string): boolean {
const [month, year] = expiryDate.split('/').map(Number);
const now = new Date();
const expiry = new Date(2000 + year, month - 1);
return expiry > now;
}
}
// PayPal Strategy
class PayPalStrategy implements PaymentStrategy {
constructor(
private clientId: string,
private clientSecret: string
) {}
async processPayment(amount: number, currency: string, metadata: Record): Promise {
try {
if (!this.validate(amount, metadata)) {
return { success: false, error: 'Invalid payment data', gateway: 'paypal' };
}
const accessToken = await this.getAccessToken();
const response = await this.createPayment(accessToken, amount, currency, metadata.paypalEmail);
return {
success: response.state === 'approved',
transactionId: response.id,
gateway: 'paypal'
};
} catch (error) {
return {
success: false,
error: error.message,
gateway: 'paypal'
};
}
}
async refund(transactionId: string, amount: number): Promise {
try {
const accessToken = await this.getAccessToken();
const response = await this.refundPayment(accessToken, transactionId, amount);
return {
success: response.state === 'completed',
transactionId: response.id,
gateway: 'paypal'
};
} catch (error) {
return {
success: false,
error: error.message,
gateway: 'paypal'
};
}
}
validate(amount: number, metadata: Record): boolean {
if (amount <= 0) return false;
if (!metadata.paypalEmail || !this.isValidEmail(metadata.paypalEmail)) return false;
return true;
}
private async getAccessToken(): Promise {
await new Promise(resolve => setTimeout(resolve, 50));
return `paypal_token_${Date.now()}`;
}
private async createPayment(token: string, amount: number, currency: string, email: string): Promise {
await new Promise(resolve => setTimeout(resolve, 150));
return {
id: `paypal_${Date.now()}`,
state: 'approved'
};
}
private async refundPayment(token: string, transactionId: string, amount: number): Promise {
await new Promise(resolve => setTimeout(resolve, 150));
return {
id: `refund_${Date.now()}`,
state: 'completed'
};
}
private isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
}
// Cryptocurrency Strategy
class CryptoStrategy implements PaymentStrategy {
constructor(private walletAddress: string) {}
async processPayment(amount: number, currency: string, metadata: Record): Promise {
try {
if (!this.validate(amount, metadata)) {
return { success: false, error: 'Invalid payment data', gateway: 'crypto' };
}
const transactionHash = await this.initiateBlockchainTransaction(
metadata.fromAddress,
this.walletAddress,
amount,
currency
);
return {
success: true,
transactionId: transactionHash,
gateway: 'crypto'
};
} catch (error) {
return {
success: false,
error: error.message,
gateway: 'crypto'
};
}
}
async refund(transactionId: string, amount: number): Promise {
// Crypto refunds typically require a reverse transaction
return {
success: false,
error: 'Crypto refunds must be processed manually',
gateway: 'crypto'
};
}
validate(amount: number, metadata: Record): boolean {
if (amount <= 0) return false;
if (!metadata.fromAddress || !this.isValidAddress(metadata.fromAddress)) return false;
return true;
}
private async initiateBlockchainTransaction(from: string, to: string, amount: number, currency: string): Promise {
await new Promise(resolve => setTimeout(resolve, 200));
return `0x${Math.random().toString(16).substr(2, 64)}`;
}
private isValidAddress(address: string): boolean {
return /^0x[a-fA-F0-9]{40}$/.test(address);
}
}
// Payment Service - Uses Strategy Pattern
class PaymentService {
private strategies: Map = new Map();
constructor() {
// Register default strategies
this.registerStrategy('creditcard', new CreditCardStrategy(
process.env.CC_API_KEY || '',
process.env.CC_MERCHANT_ID || ''
));
this.registerStrategy('paypal', new PayPalStrategy(
process.env.PAYPAL_CLIENT_ID || '',
process.env.PAYPAL_CLIENT_SECRET || ''
));
this.registerStrategy('crypto', new CryptoStrategy(
process.env.CRYPTO_WALLET || ''
));
}
registerStrategy(name: string, strategy: PaymentStrategy): void {
this.strategies.set(name, strategy);
}
async processPayment(
method: string,
amount: number,
currency: string,
metadata: Record
): Promise {
const strategy = this.strategies.get(method);
if (!strategy) {
return {
success: false,
error: `Payment method '${method}' not supported`
};
}
return strategy.processPayment(amount, currency, metadata);
}
async refund(method: string, transactionId: string, amount: number): Promise {
const strategy = this.strategies.get(method);
if (!strategy) {
return {
success: false,
error: `Payment method '${method}' not supported`
};
}
return strategy.refund(transactionId, amount);
}
}
// Usage
const paymentService = new PaymentService();
// Process payment with different strategies
const creditCardResult = await paymentService.processPayment('creditcard', 99.99, 'USD', {
cardNumber: '4111111111111111',
cvv: '123',
expiryDate: '12/25',
cardholderName: 'John Doe'
});
const paypalResult = await paymentService.processPayment('paypal', 99.99, 'USD', {
paypalEmail: 'user@example.com'
});
// === NestJS ===
import { Injectable, Inject } from '@nestjs/common';
@Injectable()
export class CreditCardStrategy implements PaymentStrategy {
// Same implementation as above
}
@Injectable()
export class PayPalStrategy implements PaymentStrategy {
// Same implementation as above
}
@Injectable()
export class PaymentService {
private strategies: Map = new Map();
constructor(
@Inject('PAYMENT_STRATEGIES') strategies: Array<{ name: string; strategy: PaymentStrategy }>
) {
strategies.forEach(({ name, strategy }) => {
this.strategies.set(name, strategy);
});
}
async processPayment(method: string, amount: number, currency: string, metadata: any): Promise {
const strategy = this.strategies.get(method);
if (!strategy) {
throw new Error(`Payment method '${method}' not supported`);
}
return strategy.processPayment(amount, currency, metadata);
}
}
// Module configuration
@Module({
providers: [
CreditCardStrategy,
PayPalStrategy,
{
provide: 'PAYMENT_STRATEGIES',
useFactory: (cc: CreditCardStrategy, pp: PayPalStrategy) => [
{ name: 'creditcard', strategy: cc },
{ name: 'paypal', strategy: pp }
],
inject: [CreditCardStrategy, PayPalStrategy]
},
PaymentService
],
exports: [PaymentService]
})
export class PaymentModule {}
12) Observer (Behavioral)
One-to-many dependency: observers get notified of subject changes.
Example
// === Node.js TypeScript ===
import { EventEmitter } from 'events';
// Event types
interface UserCreatedEvent {
userId: string;
email: string;
name: string;
createdAt: Date;
}
interface OrderCreatedEvent {
orderId: string;
userId: string;
amount: number;
items: Array<{ productId: string; quantity: number; price: number }>;
createdAt: Date;
}
interface OrderStatusChangedEvent {
orderId: string;
userId: string;
oldStatus: string;
newStatus: string;
changedAt: Date;
}
// Observer implementations
class AuditLogger {
private logFile: string;
constructor(logFile: string = './audit.log') {
this.logFile = logFile;
}
logUserCreated(event: UserCreatedEvent): void {
console.log(`[AUDIT] User created: ${event.userId} (${event.email}) at ${event.createdAt.toISOString()}`);
// Write to audit log file
}
logOrderCreated(event: OrderCreatedEvent): void {
console.log(`[AUDIT] Order created: ${event.orderId} by user ${event.userId} for $${event.amount}`);
}
logOrderStatusChange(event: OrderStatusChangedEvent): void {
console.log(`[AUDIT] Order ${event.orderId} status changed: ${event.oldStatus} -> ${event.newStatus}`);
}
}
class EmailNotificationService {
async sendWelcomeEmail(event: UserCreatedEvent): Promise {
console.log(`[EMAIL] Sending welcome email to ${event.email}...`);
// Send email implementation
await this.sendEmail(event.email, 'Welcome!', `Hello ${event.name}, welcome to our platform!`);
}
async sendOrderConfirmation(event: OrderCreatedEvent): Promise {
const user = await this.getUser(event.userId);
console.log(`[EMAIL] Sending order confirmation to ${user.email}...`);
await this.sendEmail(
user.email,
`Order Confirmation #${event.orderId}`,
`Your order for $${event.amount} has been confirmed.`
);
}
async sendOrderStatusUpdate(event: OrderStatusChangedEvent): Promise {
const user = await this.getUser(event.userId);
console.log(`[EMAIL] Sending status update to ${user.email}...`);
await this.sendEmail(
user.email,
`Order #${event.orderId} Status Update`,
`Your order status has been updated to: ${event.newStatus}`
);
}
private async sendEmail(to: string, subject: string, body: string): Promise {
// Email sending implementation
await new Promise(resolve => setTimeout(resolve, 100));
}
private async getUser(userId: string): Promise<{ email: string }> {
// Fetch user from database
return { email: 'user@example.com' };
}
}
class InventoryService {
async updateInventory(event: OrderCreatedEvent): Promise {
console.log(`[INVENTORY] Updating inventory for order ${event.orderId}...`);
for (const item of event.items) {
// Decrease stock
await this.decreaseStock(item.productId, item.quantity);
console.log(`[INVENTORY] Decreased stock for product ${item.productId} by ${item.quantity}`);
}
}
async restoreInventory(event: OrderStatusChangedEvent): Promise {
if (event.newStatus === 'cancelled') {
console.log(`[INVENTORY] Restoring inventory for cancelled order ${event.orderId}...`);
// Get order details and restore stock
const order = await this.getOrder(event.orderId);
for (const item of order.items) {
await this.increaseStock(item.productId, item.quantity);
}
}
}
private async decreaseStock(productId: string, quantity: number): Promise {
// Database update
await new Promise(resolve => setTimeout(resolve, 50));
}
private async increaseStock(productId: string, quantity: number): Promise {
// Database update
await new Promise(resolve => setTimeout(resolve, 50));
}
private async getOrder(orderId: string): Promise {
// Fetch order from database
return {
orderId,
userId: 'u1',
amount: 99.99,
items: [],
createdAt: new Date()
};
}
}
class AnalyticsService {
private metrics: Map = new Map();
trackUserCreated(event: UserCreatedEvent): void {
this.incrementMetric('users.created');
this.incrementMetric(`users.created.${new Date(event.createdAt).toISOString().split('T')[0]}`);
console.log(`[ANALYTICS] Tracked new user registration`);
}
trackOrderCreated(event: OrderCreatedEvent): void {
this.incrementMetric('orders.created');
this.incrementMetric('revenue.total', event.amount);
console.log(`[ANALYTICS] Tracked new order: $${event.amount}`);
}
trackOrderStatusChange(event: OrderStatusChangedEvent): void {
this.incrementMetric(`orders.status.${event.newStatus}`);
console.log(`[ANALYTICS] Tracked order status change: ${event.newStatus}`);
}
private incrementMetric(metric: string, value: number = 1): void {
this.metrics.set(metric, (this.metrics.get(metric) || 0) + value);
}
getMetric(metric: string): number {
return this.metrics.get(metric) || 0;
}
}
// Event Bus / Subject
class EventBus extends EventEmitter {
private auditLogger = new AuditLogger();
private emailService = new EmailNotificationService();
private inventoryService = new InventoryService();
private analyticsService = new AnalyticsService();
constructor() {
super();
// Register all observers
this.on('user.created', (event: UserCreatedEvent) => {
this.auditLogger.logUserCreated(event);
this.emailService.sendWelcomeEmail(event).catch(console.error);
this.analyticsService.trackUserCreated(event);
});
this.on('order.created', (event: OrderCreatedEvent) => {
this.auditLogger.logOrderCreated(event);
this.emailService.sendOrderConfirmation(event).catch(console.error);
this.inventoryService.updateInventory(event).catch(console.error);
this.analyticsService.trackOrderCreated(event);
});
this.on('order.status.changed', (event: OrderStatusChangedEvent) => {
this.auditLogger.logOrderStatusChange(event);
this.emailService.sendOrderStatusUpdate(event).catch(console.error);
this.inventoryService.restoreInventory(event).catch(console.error);
this.analyticsService.trackOrderStatusChange(event);
});
}
// Type-safe event emission
emitUserCreated(event: UserCreatedEvent): void {
this.emit('user.created', event);
}
emitOrderCreated(event: OrderCreatedEvent): void {
this.emit('order.created', event);
}
emitOrderStatusChanged(event: OrderStatusChangedEvent): void {
this.emit('order.status.changed', event);
}
}
// Usage
const eventBus = new EventBus();
// Services emit events
class UserService {
constructor(private eventBus: EventBus) {}
async createUser(email: string, name: string): Promise {
const userId = `user_${Date.now()}`;
// Create user in database
await this.saveUserToDatabase(userId, email, name);
// Emit event - all observers will be notified
this.eventBus.emitUserCreated({
userId,
email,
name,
createdAt: new Date()
});
return userId;
}
private async saveUserToDatabase(userId: string, email: string, name: string): Promise {
// Database save
await new Promise(resolve => setTimeout(resolve, 50));
}
}
class OrderService {
constructor(private eventBus: EventBus) {}
async createOrder(userId: string, items: Array<{ productId: string; quantity: number; price: number }>): Promise {
const orderId = `order_${Date.now()}`;
const amount = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
// Create order in database
await this.saveOrderToDatabase(orderId, userId, amount, items);
// Emit event
this.eventBus.emitOrderCreated({
orderId,
userId,
amount,
items,
createdAt: new Date()
});
return orderId;
}
async updateOrderStatus(orderId: string, userId: string, newStatus: string): Promise {
const oldStatus = await this.getOrderStatus(orderId);
// Update in database
await this.updateOrderStatusInDatabase(orderId, newStatus);
// Emit event
this.eventBus.emitOrderStatusChanged({
orderId,
userId,
oldStatus,
newStatus,
changedAt: new Date()
});
}
private async saveOrderToDatabase(orderId: string, userId: string, amount: number, items: any[]): Promise {
await new Promise(resolve => setTimeout(resolve, 50));
}
private async getOrderStatus(orderId: string): Promise {
return 'pending';
}
private async updateOrderStatusInDatabase(orderId: string, status: string): Promise {
await new Promise(resolve => setTimeout(resolve, 50));
}
}
// === NestJS ===
import { Injectable } from '@nestjs/common';
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
@Injectable()
export class UserService {
constructor(private eventEmitter: EventEmitter2) {}
async createUser(email: string, name: string): Promise {
const userId = `user_${Date.now()}`;
await this.saveUserToDatabase(userId, email, name);
// Emit typed event
this.eventEmitter.emit('user.created', {
userId,
email,
name,
createdAt: new Date()
});
return userId;
}
private async saveUserToDatabase(userId: string, email: string, name: string): Promise {
// Implementation
}
}
// Event listeners (observers)
@Injectable()
export class UserCreatedListener {
@OnEvent('user.created')
async handleUserCreated(event: UserCreatedEvent) {
console.log('User created:', event);
// Send welcome email, update analytics, etc.
}
}
@Injectable()
export class AuditListener {
@OnEvent('user.created')
logUserCreated(event: UserCreatedEvent) {
console.log(`[AUDIT] User created: ${event.userId}`);
}
@OnEvent('order.created')
logOrderCreated(event: OrderCreatedEvent) {
console.log(`[AUDIT] Order created: ${event.orderId}`);
}
}
@Injectable()
export class EmailListener {
@OnEvent('user.created')
async sendWelcomeEmail(event: UserCreatedEvent) {
// Send email
}
@OnEvent('order.created')
async sendOrderConfirmation(event: OrderCreatedEvent) {
// Send email
}
}
@Module({
imports: [EventEmitterModule.forRoot()],
providers: [UserService, UserCreatedListener, AuditListener, EmailListener],
exports: [UserService]
})
export class UserModule {}