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 ===
class DbService {
private static instance: DbService | null = null;
private constructor() { /* init pool/client once */ }
static getInstance(){ return this.instance ?? (this.instance = new DbService()); }
async query(sql: string){ return { rows: [] }; }
}
const db1 = DbService.getInstance();
const db2 = DbService.getInstance();
console.log(db1 === db2); // true
// === NestJS ===
import { Injectable } from '@nestjs/common';
@Injectable()
export class DbService {
async query(sql: string){ return { rows: [] }; }
}
// Providers are singleton-scoped by default in NestJS
2) Factory Method (Creational)
Define an interface for creating objects; subclasses decide which class to instantiate.
Example
// === Node.js TypeScript ===
interface ILogger { log(msg: string): void }
class ConsoleLogger implements ILogger { log(msg:string){ console.log(msg); } }
class FileLogger implements ILogger { log(msg:string){ require('fs').appendFileSync('app.log', msg+'\n'); } }
function createLogger(env: 'dev'|'prod'): ILogger {
return env === 'prod' ? new FileLogger() : new ConsoleLogger();
}
const logger = createLogger(process.env.NODE_ENV === 'production' ? 'prod' : 'dev');
logger.log('Server started');
// === NestJS ===
import { Injectable, Module } from '@nestjs/common';
const LOGGER_PROVIDER = {
provide: 'LOGGER',
useFactory: (): ILogger => {
return process.env.NODE_ENV === 'production' ? new FileLogger() : new ConsoleLogger();
},
};
@Module({ providers: [LOGGER_PROVIDER], 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 }
interface UserRepo { findById(id:string): Promise }
class SqlUserRepo implements UserRepo { findById(id:string){ return Promise.resolve({ id }); } }
class MongoUserRepo implements UserRepo { findById(id:string){ return Promise.resolve({ id }); } }
function createRepos(db: 'sql'|'mongo'){
return db === 'mongo' ? { userRepo: new MongoUserRepo() } : { userRepo: new SqlUserRepo() };
}
const { userRepo } = createRepos(process.env.DB === 'mongo' ? 'mongo' : 'sql');
userRepo.findById('u1').then(console.log);
// === NestJS ===
import { Module, DynamicModule } from '@nestjs/common';
@Module({})
export class ReposModule {
static register(db: 'sql'|'mongo'): DynamicModule {
const providers = db === 'mongo'
? [{ provide: 'UserRepo', useClass: MongoUserRepo }]
: [{ provide: 'UserRepo', useClass: SqlUserRepo }];
return { module: ReposModule, providers, exports: providers };
}
}
4) Builder (Creational)
Step-by-step construction for complex objects with a clear fluent API.
Example
// === Node.js TypeScript ===
class ResponseBuilder {
private statusCode = 200;
private headers: Record = {};
private body = '';
status(code:number){ this.statusCode = code; return this; }
header(k:string,v:string){ this.headers[k]=v; return this; }
json(data:T){ this.body = JSON.stringify(data); return this.header('Content-Type','application/json'); }
build(){ return { status: this.statusCode, headers: this.headers, body: this.body }; }
}
// === NestJS ===
import { Injectable } from '@nestjs/common';
@Injectable()
export class ResponseBuilder {
private statusCode = 200;
private headers: Record = {};
private body = '';
status(code:number){ this.statusCode = code; return this; }
header(k:string,v:string){ this.headers[k]=v; return this; }
json(data:T){ this.body = JSON.stringify(data); return this.header('Content-Type','application/json'); }
build(){ return { status: this.statusCode, headers: this.headers, body: this.body }; }
}
5) Prototype (Creational)
Create new objects by cloning existing ones.
Example
// === Node.js TypeScript ===
interface UserDto { id: string; roles: string[] }
const original: UserDto = { id: 'u1', roles: ['admin'] };
const clone: UserDto = structuredClone(original);
clone.roles = [...clone.roles, 'auditor'];
// === NestJS ===
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService {
cloneUser(user: UserDto): UserDto {
return structuredClone(user);
}
}
6) Adapter (Structural)
Convert one interface into another clients expect.
Example
// === Node.js TypeScript ===
interface FileStorage { upload(path: string, bytes: Buffer): Promise }
class LegacyS3 { putObject(p:string, b:Buffer){ return Promise.resolve(`s3://${p}`); } }
class S3Adapter implements FileStorage {
constructor(private legacy = new LegacyS3()) {}
upload(path: string, bytes: Buffer){ return this.legacy.putObject(path, bytes); }
}
const storage: FileStorage = new S3Adapter();
storage.upload('imgs/a.png', Buffer.from('data')).then(console.log);
// === NestJS ===
import { Injectable } from '@nestjs/common';
@Injectable()
export class S3Adapter implements FileStorage {
constructor(private legacy = new LegacyS3()) {}
upload(path: string, bytes: Buffer){ return this.legacy.putObject(path, bytes); }
}
7) Facade (Structural)
Provide a simple interface to a complex subsystem.
Example
// === Node.js TypeScript ===
class UsersService { findById(id:string){ return Promise.resolve({ id }); } }
class OrdersService { findByUser(id:string){ return Promise.resolve([]); } }
class AccountFacade {
constructor(private users = new UsersService(), private orders = new OrdersService()) {}
async getAccountOverview(userId: string){
const [user, orders] = await Promise.all([
this.users.findById(userId),
this.orders.findByUser(userId)
]);
return { user, orders };
}
}
new AccountFacade().getAccountOverview('u1').then(console.log);
// === NestJS ===
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService { findById(id:string){ return Promise.resolve({ id }); } }
@Injectable()
export class OrdersService { findByUser(id:string){ return Promise.resolve([]); } }
@Injectable()
export class AccountFacade {
constructor(private users: UsersService, private orders: OrdersService) {}
async getAccountOverview(userId: string){
const [user, orders] = await Promise.all([
this.users.findById(userId),
this.orders.findByUser(userId)
]);
return { user, orders };
}
}
8) Proxy (Structural)
Control access to another object (cache, lazy, auth).
Example
// === Node.js TypeScript ===
function withCache(fn: (...a:TArgs)=>Promise){
const cache = new Map();
return async (...args: TArgs): Promise => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key)!;
const result = await fn(...args);
cache.set(key, result);
return result;
};
}
const fetchUser = async (id:string)=> ({ id, name:'Alice' });
const fetchUserCached = withCache(fetchUser);
fetchUserCached('u1').then(console.log);
// === NestJS ===
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
private cache = new Map();
intercept(ctx: ExecutionContext, next: CallHandler): Observable {
const req = ctx.switchToHttp().getRequest();
const key = req.method+':'+req.url;
if (this.cache.has(key)) return of(this.cache.get(key));
return next.handle().pipe(tap(data => this.cache.set(key, data)));
}
}
9) Decorator (Structural)
Add responsibilities to objects dynamically without subclassing.
Example
// === Node.js TypeScript ===
function Time(){
return function(_target:any, _key:string, descriptor: PropertyDescriptor){
const orig = descriptor.value;
descriptor.value = async function(...args:any[]){
const t0 = Date.now();
const result = await orig.apply(this, args);
console.log(`${_key} took ${Date.now()-t0}ms`);
return result;
};
};
}
class Service {
@Time()
async work(){ return new Promise(r=>setTimeout(()=>r('done'), 50)); }
}
// === NestJS ===
import { Injectable } from '@nestjs/common';
@Injectable()
export class Service {
@Time()
async work(){ return new Promise(r=>setTimeout(()=>r('done'), 50)); }
}
10) Composite (Structural)
Treat individual objects and compositions uniformly.
Example
// === Node.js TypeScript ===
interface Component { sum(): number }
class Leaf implements Component { constructor(private v:number){} sum(){ return this.v; } }
class Group implements Component {
constructor(private children: Component[] = []){}
add(c: Component){ this.children.push(c); }
sum(){ return this.children.reduce((s,c)=> s + c.sum(), 0); }
}
const root = new Group([ new Leaf(1), new Group([ new Leaf(2), new Leaf(3) ]) ]);
console.log(root.sum());
// === NestJS ===
import { Injectable } from '@nestjs/common';
@Injectable()
export class Leaf implements Component { constructor(private v:number){} sum(){ return this.v; } }
@Injectable()
export class Group implements Component {
constructor(private children: Component[] = []){}
add(c: Component){ this.children.push(c); }
sum(){ return this.children.reduce((s,c)=> s + c.sum(), 0); }
}
11) Strategy (Behavioral)
Define a family of algorithms; make them interchangeable.
Example
// === Node.js TypeScript ===
interface PaymentStrategy { pay(amountCents: number): Promise }
class CreditCardStrategy implements PaymentStrategy { async pay(n:number){ return true; } }
class PaypalStrategy implements PaymentStrategy { async pay(n:number){ return true; } }
class OrderService {
constructor(private strategy: PaymentStrategy){ }
checkout(amountCents:number){ return this.strategy.pay(amountCents); }
}
const strategy: PaymentStrategy = process.env.PAY === 'paypal' ? new PaypalStrategy() : new CreditCardStrategy();
new OrderService(strategy).checkout(4999).then(console.log);
// === NestJS ===
import { Injectable, Inject } from '@nestjs/common';
@Injectable()
export class OrderService {
constructor(@Inject('PAYMENT_STRATEGY') private strategy: PaymentStrategy) {}
checkout(amountCents: number){ return this.strategy.pay(amountCents); }
}
// In module: { provide: 'PAYMENT_STRATEGY', useClass: CreditCardStrategy }
12) Observer (Behavioral)
One-to-many dependency: observers get notified of subject changes.
Example
// === Node.js TypeScript ===
import { EventEmitter } from 'events';
const events = new EventEmitter();
events.on('user.created', (payload:{ userId: string }) => {
console.log('audit', payload.userId);
});
function createUser(userId: string){
// ...create in DB
events.emit('user.created', { userId });
}
createUser('u1');
// === NestJS ===
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
@Injectable()
export class UserService {
constructor(private events: EventEmitter2) {}
createUser(userId: string){ this.events.emit('user.created', { userId }); }
}
// Listener: @OnEvent('user.created') handle(evt: { userId: string }) { ... }