Skip to content
Node.js nd patterns 4 min read

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.

CategoryConcernRepresentative patterns
CreationalHow objects are created and instantiatedFactory, Singleton, Builder, Prototype
StructuralHow objects and classes are composedAdapter, Decorator, Facade, Proxy, Module
BehavioralHow objects communicate and share responsibilityObserver, 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/await over nested callbacks when implementing patterns. It keeps control flow linear and makes error handling with try/catch straightforward.

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: EventEmitter for 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.
Last updated June 14, 2026
Was this helpful?