Design Patterns in Node.js
Design patterns are reusable, battle-tested solutions to recurring problems in software design. They give teams a shared vocabulary, encode hard-won experience, and help you reason about structure before you write a single line of code. In Node.js the patterns you already know from object-oriented languages still apply, but JavaScript’s dynamic typing, first-class functions, prototypal inheritance, and event-driven asynchronous core reshape how each one is implemented — often making them far lighter than their classical counterparts.
What is a design pattern?
A design pattern is a description of a solution to a common design problem, framed so it can be adapted to many situations. The term was popularized by the 1994 book Design Patterns: Elements of Reusable Object-Oriented Software by the “Gang of Four” (GoF). A pattern is not a finished piece of code you copy; it is a template — a name, the problem it addresses, the structure of the solution, and the trade-offs involved.
Patterns matter because they raise the level of conversation. Saying “let’s put a Factory here” communicates intent instantly, whereas describing the same construction logic in prose takes a paragraph. They also steer you toward designs that are easier to test, extend, and reason about.
Patterns are tools, not goals. Reaching for a pattern when a plain function or module would do adds ceremony without value. Apply them to solve a real, present problem.
The three classic categories
The GoF patterns are grouped into three families based on what they primarily address.
| Category | Concern | Representative patterns |
|---|---|---|
| Creational | How objects are created and instantiated | Factory, Singleton, Builder, Prototype |
| Structural | How objects and classes are composed | Adapter, Decorator, Facade, Proxy, Module |
| Behavioral | How objects communicate and share responsibility | Observer, Strategy, Command, Iterator, Middleware |
Creational patterns
Creational patterns abstract the process of instantiation, decoupling a system from the specifics of how its objects are made. In Node.js, where a function can return any object and there is no new-only construction, a factory is frequently just a function.
// factory.js — a creational pattern as a plain function
export function createUser({ name, role = "member" }) {
return {
name,
role,
isAdmin: role === "admin",
greet() {
return `Hi, I'm ${name} (${role})`;
},
};
}
const admin = createUser({ name: "Ada", role: "admin" });
console.log(admin.greet(), "| admin?", admin.isAdmin);
Output:
Hi, I'm Ada (admin) | admin? true
Structural patterns
Structural patterns describe how to assemble objects and classes into larger structures while keeping them flexible. The Module pattern — encapsulating private state and exposing a public surface — is so fundamental that Node’s CommonJS and ES module systems are built around it.
// counter.js — module pattern with truly private state
let count = 0; // not exported, so it stays private
export function increment() {
count += 1;
return count;
}
export function current() {
return count;
}
// main.js
import { increment, current } from "./counter.js";
increment();
increment();
console.log("count is", current());
Output:
count is 2
Behavioral patterns
Behavioral patterns focus on communication between objects and the assignment of responsibilities. The Observer pattern is woven directly into Node’s core via the EventEmitter class, making publish/subscribe a native idiom rather than something you build by hand.
import { EventEmitter } from "node:events";
const bus = new EventEmitter();
bus.on("order:created", (order) => {
console.log(`Charging card for order #${order.id} ($${order.total})`);
});
bus.emit("order:created", { id: 1024, total: 49.99 });
Output:
Charging card for order #1024 ($49.99)
How JavaScript and Node reshape the patterns
Several language and runtime features mean that classical, class-heavy implementations are often unnecessary in Node.
- First-class functions. Strategy, Command, and Factory frequently collapse into passing a function as an argument or returning one, removing the need for a class per variation.
- Closures over private fields. The Module and Singleton patterns lean on closures (or ES module top-level scope) for genuine encapsulation, instead of access modifiers.
- Dynamic typing and duck typing. Adapters and decorators can wrap any object that exposes the right shape, with no interface declarations to satisfy.
- A single module instance by default. Node caches modules after first load, so a module that exports an object is effectively a Singleton without extra code.
The async dimension
Node’s defining characteristic is its non-blocking, event-driven model. This adds an “async” flavor to traditional patterns: callbacks gave way to Promises and async/await, streams implement an iterator-like pull/push contract, and middleware pipelines (as in Express or Koa) are a behavioral pattern purpose-built for asynchronous request handling.
// A strategy pattern where each strategy is an async function
const fetchers = {
rest: async (id) => (await fetch(`https://api.example.com/users/${id}`)).json(),
cache: async (id) => ({ id, name: "cached" }),
};
async function getUser(source, id) {
const strategy = fetchers[source] ?? fetchers.cache;
return strategy(id);
}
console.log(await getUser("cache", 7));
Output:
{ id: 7, name: 'cached' }
Prefer
async/awaitover nested callbacks when implementing patterns. It keeps control flow linear and makes error handling withtry/catchstraightforward.
Best practices
- Choose a pattern to solve a concrete problem you have today, not one you imagine you might have later.
- Favor the lightest expression of a pattern — a function or a module often beats a class hierarchy in JavaScript.
- Lean on built-ins:
EventEmitterfor Observer, the module system for Module/Singleton, streams for Iterator-style flows. - Embrace the async model; model strategies, commands, and pipelines as Promise-returning functions.
- Name things after the pattern when it aids communication, but don’t let the label drive over-engineering.
- Keep patterns testable — dependency boundaries created by Factory and Dependency Injection make mocking trivial.