All Design Pattern Javascript/Typescript You Must Know
Structural Patterns with Best Practice API Usage
1. Composite Pattern + API Fetching
Best practice use case:
Imagine a system with a tree of components (e.g. widgets, dashboards) where each widget fetches its own API data.
Example:
interface Component {
render(): Promise<void>;
}
class APITile implements Component {
constructor(private url: string) {}
async render() {
const res = await fetch(this.url);
const data = await res.json();
console.log("Tile data:", data);
}
}
class Dashboard implements Component {
private children: Component[] = [];
add(component: Component) {
this.children.push(component);
}
async render() {
console.log("Rendering Dashboard...");
await Promise.all(this.children.map(child => child.render()));
}
}
// Usage
const dashboard = new Dashboard();
dashboard.add(new APITile("https://jsonplaceholder.typicode.com/users/1"));
dashboard.add(new APITile("https://jsonplaceholder.typicode.com/posts/1"));
dashboard.render();
Best practice:
- Use async/await cleanly — always wrap API calls in try-catch if needed.
- Keep each component responsible for its own fetch.
2. Decorator Pattern + Axios Logging Decorator
Best practice use case:
Wrap API calls with logging, caching, or retry decorators without touching core logic.
Example:
import axios from 'axios';
interface APIService {
get(url: string): Promise<any>;
}
class RealAPIService implements APIService {
async get(url: string) {
const response = await axios.get(url);
return response.data;
}
}
class LoggingDecorator implements APIService {
constructor(private service: APIService) {}
async get(url: string) {
console.log(`[LOG] Fetching: ${url}`);
const result = await this.service.get(url);
console.log(`[LOG] Fetched:`, result);
return result;
}
}
// Usage
const api = new LoggingDecorator(new RealAPIService());
api.get('https://jsonplaceholder.typicode.com/users/1');
Best practice:
- Use decorators to transparently enhance behavior.
- Keep your API service and cross-cutting concerns decoupled.
3. Adapter Pattern + API Integration
Best practice use case:
When adapting legacy or third-party API responses to your app’s expected format.
Example:
interface User {
id: number;
fullName: string;
}
class UserAPI {
async fetchUser(): Promise<any> {
const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
return response.json();
}
}
class UserAdapter {
constructor(private api: UserAPI) {}
async getUser(): Promise<User> {
const data = await this.api.fetchUser();
return {
id: data.id,
fullName: `${data.name}`
};
}
}
// Usage
const userAdapter = new UserAdapter(new UserAPI());
userAdapter.getUser().then(user => console.log(user));
Best practice:
- Use adapters to decouple your domain models from external APIs.
- Ensure consistent internal interfaces even if external APIs are inconsistent.
4. Bridge Pattern + API for Multiple Backends
Best practice use case:
Support multiple backends (e.g. REST and GraphQL) behind a common abstraction.
Example:
interface UserService {
getUser(id: number): Promise<any>;
}
class RestUserService implements UserService {
async getUser(id: number) {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
return res.json();
}
}
class GraphQLUserService implements UserService {
async getUser(id: number) {
const query = `{ user(id: ${id}) { id, name } }`;
const res = await fetch('/graphql', {
method: 'POST',
body: JSON.stringify({ query }),
headers: { 'Content-Type': 'application/json' }
});
const data = await res.json();
return data.data.user;
}
}
// Usage
function displayUser(service: UserService, id: number) {
service.getUser(id).then(console.log);
}
displayUser(new RestUserService(), 1);
// displayUser(new GraphQLUserService(), 1);
Best practice:
- Use common interfaces for services.
- Keep your code backend-agnostic for future flexibility.
5. Façade Pattern + Unified API Service
Best practice use case:
Simplify complex multi-endpoint operations behind a single clean interface.
Example:
class UserAPI {
getUser(id: number) {
return fetch(`https://jsonplaceholder.typicode.com/users/${id}`).then(res => res.json());
}
}
class PostAPI {
getPost(id: number) {
return fetch(`https://jsonplaceholder.typicode.com/posts/${id}`).then(res => res.json());
}
}
class APIServiceFacade {
private userAPI = new UserAPI();
private postAPI = new PostAPI();
async getUserWithPost(userId: number, postId: number) {
const [user, post] = await Promise.all([
this.userAPI.getUser(userId),
this.postAPI.getPost(postId)
]);
return { user, post };
}
}
// Usage
const apiFacade = new APIServiceFacade();
apiFacade.getUserWithPost(1, 1).then(console.log);
Best practice:
- Expose simple, high-level APIs to the app layer.
- Hide underlying multi-API orchestration complexity.
6. Proxy Pattern + Lazy API Loader
Best practice use case:
Lazy-load API data only when needed, or cache results.
Example:
interface DataService {
getData(): Promise<any>;
}
class RealDataService implements DataService {
async getData() {
const res = await fetch("https://jsonplaceholder.typicode.com/posts/1");
return res.json();
}
}
class ProxyDataService implements DataService {
private realService: RealDataService | null = null;
async getData() {
if (!this.realService) {
console.log("Initializing RealDataService...");
this.realService = new RealDataService();
}
return this.realService.getData();
}
}
// Usage
const service = new ProxyDataService();
service.getData().then(console.log);
Best practice:
- Use proxies for caching, lazy loading, or access control around API calls.
- Defer resource-heavy operations until necessary.
Summary: Clean API-Driven Structural Patterns
Pattern | Best API Usage Scenario |
---|---|
Composite | Independent nested components fetching their own APIs |
Decorator | Transparent logging, caching, retry around API calls |
Adapter | Adapting external API responses to your own model |
Bridge | Switching between REST, GraphQL, or other backends |
Façade | Simplifying multi-endpoint orchestration behind one service |
Proxy | Lazy-load or cache API data behind a proxy service |
Final Best Practices (Middle-to-Senior)
- Always abstract API endpoints behind interfaces (clean dependency inversion)
- Use decorators/proxies for logging, caching, retries, authorization
- Facades and adapters are perfect for safely evolving 3rd-party or legacy APIs
- Use async/await, Promise.all smartly — optimize for parallel requests
- Apply SOLID principles alongside these patterns for clean, scalable design
Behavioral Design Patterns with Best Practice API Workflows
Behavioral patterns manage how objects interact, coordinate, and execute actions together. In API-driven apps, this often means:
- Handling async calls
- Chaining actions
- Managing retries, commands, event systems
- Centralizing coordination
I’ll show clean examples you can apply in your projects right now.
1. Chain of Responsibility + API Retry Chain
When to use:
When multiple handlers/processors should get a chance to handle an API request, like retrying with fallback URLs.
Example:
interface APIHandler {
setNext(handler: APIHandler): APIHandler;
handle(url: string): Promise<any>;
}
class PrimaryAPIHandler implements APIHandler {
private nextHandler: APIHandler | null = null;
setNext(handler: APIHandler): APIHandler {
this.nextHandler = handler;
return handler;
}
async handle(url: string): Promise<any> {
try {
const res = await fetch(url);
if (!res.ok) throw new Error("Primary failed");
return res.json();
} catch {
if (this.nextHandler) return this.nextHandler.handle(url);
throw new Error("All attempts failed");
}
}
}
class FallbackAPIHandler implements APIHandler {
async handle(url: string): Promise<any> {
console.log("Trying fallback...");
const fallbackUrl = url.replace("primary", "fallback");
const res = await fetch(fallbackUrl);
if (!res.ok) throw new Error("Fallback failed");
return res.json();
}
}
// Usage
const primary = new PrimaryAPIHandler();
primary.setNext(new FallbackAPIHandler());
primary.handle("https://primary.api/users/1")
.then(data => console.log(data))
.catch(err => console.error(err));
Best practice:
- Chain handlers cleanly — don’t hardcode retry logic inside a single method.
- Fail gracefully with fallback options.
2. Command Pattern + Queued API Calls
When to use:
To encapsulate API actions as command objects, enabling queuing, undo, or batching.
Example:
interface Command {
execute(): Promise<void>;
}
class FetchUserCommand implements Command {
constructor(private userId: number) {}
async execute() {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${this.userId}`);
const data = await res.json();
console.log("User:", data);
}
}
class APICommandQueue {
private queue: Command[] = [];
addCommand(command: Command) {
this.queue.push(command);
}
async run() {
for (const command of this.queue) {
await command.execute();
}
}
}
// Usage
const queue = new APICommandQueue();
queue.addCommand(new FetchUserCommand(1));
queue.addCommand(new FetchUserCommand(2));
queue.run();
Best practice:
- Use command queues for offline queues, retry systems, or batch job processing.
- Keep commands simple and self-contained.
3. Observer Pattern + API Polling / WebSocket Events
When to use:
When multiple parts of your app need to react to API events or server updates.
Example:
type Listener = (data: any) => void;
class APIObserver {
private listeners: Listener[] = [];
subscribe(listener: Listener) {
this.listeners.push(listener);
}
unsubscribe(listener: Listener) {
this.listeners = this.listeners.filter(l => l !== listener);
}
notify(data: any) {
this.listeners.forEach(listener => listener(data));
}
async pollAPI() {
setInterval(async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/posts/1");
const data = await res.json();
this.notify(data);
}, 5000);
}
}
// Usage
const observer = new APIObserver();
observer.subscribe(data => console.log("Subscriber 1", data));
observer.subscribe(data => console.log("Subscriber 2", data));
observer.pollAPI();
Best practice:
- Use observers for push-based API updates (WebSockets, polling).
- Decouple event sources from event handlers.
4. Strategy Pattern + Dynamic API Request Policies
When to use:
To choose different API request strategies (e.g., cached, no-cache, retrying) at runtime.
Example:
interface FetchStrategy {
fetch(url: string): Promise<any>;
}
class NormalFetch implements FetchStrategy {
async fetch(url: string) {
const res = await fetch(url);
return res.json();
}
}
class CachedFetch implements FetchStrategy {
private cache: Map<string, any> = new Map();
async fetch(url: string) {
if (this.cache.has(url)) {
console.log("Returning from cache");
return this.cache.get(url);
}
const res = await fetch(url);
const data = await res.json();
this.cache.set(url, data);
return data;
}
}
class APIContext {
constructor(private strategy: FetchStrategy) {}
fetch(url: string) {
return this.strategy.fetch(url);
}
setStrategy(strategy: FetchStrategy) {
this.strategy = strategy;
}
}
// Usage
const api = new APIContext(new NormalFetch());
api.fetch("https://jsonplaceholder.typicode.com/users/1").then(console.log);
api.setStrategy(new CachedFetch());
api.fetch("https://jsonplaceholder.typicode.com/users/1").then(console.log);
Best practice:
- Use strategies for switchable API request behaviors without changing client logic.
5. Mediator Pattern + Centralized API Coordinator
When to use:
To centralize communication between different services or components making API calls.
Example:
class APIMediator {
private services: Record<string, any> = {};
register(name: string, service: any) {
this.services[name] = service;
}
async notify(sender: string, action: string, ...args: any[]) {
switch (action) {
case "userFetched":
this.services["PostService"].fetchPosts(args[0]);
break;
}
}
}
class UserService {
constructor(private mediator: APIMediator) {}
async fetchUser(id: number) {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
const user = await res.json();
console.log("User fetched:", user);
this.mediator.notify("UserService", "userFetched", user.id);
}
}
class PostService {
async fetchPosts(userId: number) {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`);
const posts = await res.json();
console.log("Posts fetched:", posts);
}
}
// Usage
const mediator = new APIMediator();
const userService = new UserService(mediator);
const postService = new PostService();
mediator.register("PostService", postService);
userService.fetchUser(1);
Best practice:
- Mediators prevent tight coupling between services — one service needn’t know who to call next.
Summary — Which API Pattern When
Pattern | API Workflow Example |
---|---|
Chain of Responsibility | Retry chains, fallback APIs, interceptors |
Command | API command queues, undoable actions |
Observer | WebSocket notifications, API polling listeners |
Strategy | Dynamic API request behaviors (cached, no-cache) |
Mediator | Centralized coordination between services |
Final API Best Practices
- Always wrap API calls in services using clean interfaces or classes.
- Use async/await consistently, and Promise.all for parallel requests.
- Build resilient systems with retries, fallback handlers, and observers.
- Apply SOLID principles and patterns to keep API-heavy apps modular and testable.
- Use mediators and command queues for workflows and background jobs.
Full-Stack TypeScript Clean Architecture Plan
Tech stack:
- Backend: NestJS (TypeScript, modular, DI-friendly)
- Frontend: Next.js (React + TypeScript)
- API Protocol: REST (optionally extendable to GraphQL)
- Patterns: Factory, Singleton, Adapter, Decorator, Proxy, Strategy, Chain of Responsibility, Command, Observer, Mediator
- Tools: Axios, Prisma or TypeORM, Zod/DTO validation, Swagger, JWT
1. Modular API Layer (Backend — NestJS)
Backend App Modules
src/
modules/
user/
user.module.ts
user.controller.ts
user.service.ts
user.repository.ts
dto/
strategy/
observer/
post/
auth/
common/
middleware/
guards/
interceptors/
exceptions/
config/
main.ts
Best practices:
- Keep modules independent
- Implement middleware chains
- Centralize error handling
- Use DTOs + validation
- Isolate repositories
- Apply design patterns within services
2. Example: API Retry Chain (Chain of Responsibility)
/common/api/api-retry.handler.ts
export interface APIHandler {
setNext(handler: APIHandler): APIHandler;
handle(url: string): Promise<any>;
}
export class PrimaryHandler implements APIHandler {
private nextHandler: APIHandler | null = null;
setNext(handler: APIHandler): APIHandler {
this.nextHandler = handler;
return handler;
}
async handle(url: string) {
try {
return await fetch(url).then(res => res.json());
} catch {
if (this.nextHandler) return this.nextHandler.handle(url);
throw new Error('API failed');
}
}
}
Use this inside your NestJS services.
3. API Command Queue (Command Pattern)
/common/commands/api-command.ts
export interface Command {
execute(): Promise<void>;
}
export class GetUserCommand implements Command {
constructor(private userService: UserService, private id: number) {}
async execute() {
const user = await this.userService.getUserById(this.id);
console.log('User fetched:', user);
}
}
Use a queue to process these for background jobs or sequential workflows.
4. API Observer Service
/modules/post/post.observer.ts
type Listener = (data: any) => void;
export class PostObserver {
private listeners: Listener[] = [];
subscribe(listener: Listener) {
this.listeners.push(listener);
}
notify(data: any) {
this.listeners.forEach(listener => listener(data));
}
}
Notify other modules when new posts are created.
5. Auth Strategy Pattern
/modules/auth/strategy/jwt.strategy.ts
@Injectable()
export class JWTStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: configService.get('JWT_SECRET'),
});
}
async validate(payload: any) {
return { userId: payload.sub, email: payload.email };
}
}
Best practices:
- Decouple auth strategies (JWT, session, OAuth)
- Keep authentication strategies pluggable
6. Error Handling (Proxy + Decorator)
Global Exception Filter
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
response.status(exception.status || 500).json({
statusCode: exception.status || 500,
message: exception.message || 'Unexpected error occurred',
});
}
}
Best practices:
- Centralize error formatting
- Use interceptors or decorators for logging and validation
7. Middleware, Guards, Interceptors
/common/middleware/logging.middleware.ts
@Injectable()
export class LoggingMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`[${req.method}] ${req.originalUrl}`);
next();
}
}
/common/guards/auth.guard.ts
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
return !!request.user;
}
}
Best practices:
- Protect routes with guards
- Use interceptors for request/response transformations
- Apply middleware chains early in the lifecycle
8. Next.js Frontend Architecture
/services/userService.ts
import axios from 'axios';
export const userService = {
async getUser(id: number) {
const res = await axios.get(`/api/users/${id}`);
return res.data;
}
}
/hooks/useUser.ts
import { useEffect, useState } from 'react';
import { userService } from '../services/userService';
export function useUser(id: number) {
const [user, setUser] = useState(null);
useEffect(() => {
userService.getUser(id).then(setUser);
}, [id]);
return user;
}
Best practices:
- Keep services isolated
- Use React hooks for stateful API data
- Use decorators for logging or retry wrappers on services
9. Full-stack Integration Patterns
Problem | Pattern Solution |
---|---|
API retries/fallbacks | Chain of Responsibility |
Central event management | Mediator + Observer |
Auth strategies | Strategy Pattern |
API queuing & batching | Command Pattern |
Modular service coordination | Mediator Pattern |
Lazy or cached API calls | Proxy + Decorators |
Error Handling + Logging | Interceptor + Global Filter |
Customizable requests | Decorator + Adapter |
Frontend service isolation | Facade + Singleton |