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.

Use: shared config/cache/loggerPros: one source of truth
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.

Use: choose variant at runtimePros: decouples construction
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.

Use: theme/brand familiesPros: consistency across variants
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.

Use: complex object setupPros: readable, flexible
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.

Use: copy configured instancesPros: cheap duplication
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.

Use: integrate third-partyPros: decouple client
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.

Use: simplify APIsPros: easier onboarding
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).

Use: caching/permissionPros: performance, safety
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.

Use: optional featuresPros: flexible composition
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.

Use: trees/menusPros: recursive ops
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.

Use: pluggable behaviorPros: open/closed
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.

Use: events/reactivityPros: decoupled updates
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 }) { ... }