Architecture & Design Basics
8 examples to get you started with Architecture & Design for Node.js backends - 6 basic and 2 intermediate.
Search across all documentation pages
8 examples to get you started with Architecture & Design for Node.js backends - 6 basic and 2 intermediate.
These examples assume Node.js 24.18.0, TypeScript 5.6+, and a small Express 5 or Fastify 5 API.
mkdir orders-api && cd orders-api
npm init -y
npm install express@5 zod
npm install -D typescript@5.6 @types/node @types/express tsx
npx tsc --initOne deployable, clear module boundaries inside the repo.
src/
├── modules/
│ ├── orders/
│ │ ├── domain/
│ │ ├── application/
│ │ └── infrastructure/
│ └── billing/
├── shared/
└── main.tsRelated: Modular Monolith - package-by-feature inside one deployable
Routes translate HTTP; domain code stays framework-agnostic.
// src/modules/orders/application/create-order.ts
export type CreateOrderInput = { customerId: string; sku: string; qty: number };
export function createOrder(input: CreateOrderInput) {
if (input.qty <= 0) throw new Error("qty must be positive");
return { id: crypto.randomUUID(), ...input, status: "pending" as const };
}// src/modules/orders/infrastructure/http/routes.ts
import express from "express";
import { createOrder } from "../../application/create-order";
export const ordersRouter = express.Router();
ordersRouter.post("/", (req, res) => {
const order = createOrder(req.body);
res.status(201).json({ data: order });
});createOrder has no Request or Response imports - unit tests run without HTTPRelated: Hexagonal Architecture in Node - ports and adapters layout
A port is an interface the domain depends on; adapters implement it.
// src/modules/orders/domain/ports/order-repository.ts
export type Order = { id: string; customerId: string; sku: string; qty: number };
export interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
}// src/modules/orders/infrastructure/postgres-order-repository.ts
import type { Order, OrderRepository } from "../domain/ports/order-repository";
export class PostgresOrderRepository implements OrderRepository {
constructor(private readonly pool: { query: (sql: string, params: unknown[]) => Promise<unknown> }) {}
async save(order: Order): Promise<void> {
await this.pool.query(
"INSERT INTO orders (id, customer_id, sku, qty) VALUES ($1,$2,$3,$4)",
[order.id, order.customerId, order.sku, order.qty]
);
}
async findById(id: string): Promise<Order | null> {
const rows = await this.pool.query("SELECT * FROM orders WHERE id = $1", [id]);
return (rows as Order[])[0] ?? null;
}
}pg, or Redis clientsGroup everything for "orders" together instead of a global controllers/ tree.
src/modules/orders/
├── domain/
│ ├── order.ts
│ └── ports/
├── application/
│ ├── create-order.ts
│ └── list-orders.ts
└── infrastructure/
├── http/routes.ts
└── postgres-order-repository.tsimport/no-restricted-paths can enforce module boundariesRelated: Modular Monolith - enforcing boundaries in one repo
Distributed systems buy independence at the cost of network, ops, and consistency.
| Signal | Stay monolith | Consider services |
|---|---|---|
| Team size | 1-3 squads on one product | 4+ squads stepping on same deploy |
| Deploy cadence | Weekly or daily shared release | Need hourly deploys per domain |
| Data coupling | Shared transactions across features | Clear bounded contexts with rare cross-joins |
| Ops maturity | Single on-call rotation | Platform team for mesh, tracing, SLOs |
Related: Microservices When Worth It - team and cost trade-offs
Architecture Decision Records survive reorgs and memory loss.
# ADR-003: Stay Modular Monolith for 2026
## Status
Accepted
## Context
Three squads, 40k RPS peak, shared Postgres. No dedicated platform team.
## Decision
Remain a modular monolith. Revisit when billing needs independent deploy cadence.
## Consequences
+ Faster feature delivery, simpler debugging
- Billing deploys wait on orders releasesRelated: ADR: Monolith vs Services - Node-specific template
The composition root is the only place that knows concrete implementations.
// src/main.ts
import express from "express";
import { PostgresOrderRepository } from "./modules/orders/infrastructure/postgres-order-repository";
import { ordersRouter } from "./modules/orders/infrastructure/http/routes";
const pool = { query: async () => [] }; // replace with real pg.Pool
const orderRepo = new PostgresOrderRepository(pool);
const app = express();
app.use(express.json());
app.use("/orders", ordersRouter);
app.listen(3000);main.ts or a dedicated composition-root.tsRun a structural checklist before extracting a service.
// scripts/architecture-audit.ts - grep for cross-module imports
import fs from "node:fs";
import path from "node:path";
const modulesDir = "src/modules";
for (const mod of fs.readdirSync(modulesDir)) {
const files = fs.readdirSync(path.join(modulesDir, mod), { recursive: true }) as string[];
const tsFiles = files.filter((f) => f.endsWith(".ts"));
console.log(`${mod}: ${tsFiles.length} files`);
}index.ts re-exports everything, fix modularity inside the monolith firstRelated: Refactoring Checklist - god
index.tsand route sprawl audits
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.
Reviewed by Chris St. John·Last updated Jul 16, 2026