Modularity Basics
7 examples to get you started with Modularity in Node.js backends - 5 basic and 2 intermediate.
Prerequisites
TypeScript 5.6+, Express 5 or Fastify 5, and a src/ folder with at least one API route.
npm install express@5 zod
npm install -D typescript@5.6 tsxBasic Examples
1. Thin Route Handler
The route parses HTTP and delegates.
// infrastructure/http/orders-router.ts
import express from "express";
import { createOrder } from "../../application/create-order";
export const ordersRouter = express.Router();
ordersRouter.post("/", async (req, res, next) => {
try {
const order = await createOrder(req.body);
res.status(201).json({ data: order });
} catch (err) {
next(err);
}
});- Handler under 10 lines is the target
createOrderhas no Express imports- Errors flow to centralized error middleware
Related: Use Cases & Services - application layer
2. Use Case Function With Explicit Dependencies
// application/create-order.ts
import type { OrderRepository } from "../domain/ports/order-repository";
export type CreateOrderDeps = { orders: OrderRepository };
export async function createOrder(
deps: CreateOrderDeps,
input: { customerId: string; sku: string; qty: number }
) {
if (input.qty <= 0) throw new Error("INVALID_QTY");
const order = { id: crypto.randomUUID(), ...input, status: "pending" as const };
await deps.orders.save(order);
return order;
}- Dependencies passed explicitly - no hidden singletons
- Unit test passes in-memory
OrderRepository - Domain errors thrown as typed errors in larger apps
3. Port Interface for Persistence
// domain/ports/order-repository.ts
export type Order = {
id: string;
customerId: string;
sku: string;
qty: number;
status: "pending" | "shipped";
};
export interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
}- Application depends on interface, not Prisma
- Adapter in
infrastructure/implements the port - Swapping Postgres for DynamoDB changes one file
Related: Repository Pattern - test doubles
4. Forbidden Import Direction
Allowed:
infrastructure/http -> application -> domain
Forbidden:
domain -> express
domain -> infrastructure
application -> express Request type- ESLint
no-restricted-importsencodes the rule in CI - Framework types stop at the HTTP adapter
- Keeps use cases callable from CLI and queue workers
Related: Modularity Best Practices - enforcement checklist
5. Composition Root Wires Dependencies
// main.ts
import express from "express";
import { ordersRouter } from "./infrastructure/http/orders-router";
import { PostgresOrderRepository } from "./infrastructure/postgres-order-repository";
import { createOrderHandler } from "./infrastructure/http/create-order-handler";
const orders = new PostgresOrderRepository(pool);
const app = express();
app.use(express.json());
app.use("/orders", createOrderHandler({ orders }));
app.listen(3000);- Only
main.tsknows concrete classes - Handlers close over deps via factory functions
- Integration tests build alternate composition root
Related: Dependency Injection Patterns - manual vs Awilix vs Nest
Intermediate Examples
6. Shared Kernel vs Feature Module
src/
├── modules/orders/ # feature module - owns order rules
├── modules/billing/
└── shared/
├── errors.ts # AppError base class
└── logger.ts # pino instanceshared/has no business rules like tax calculation- If two modules need tax logic,
billingowns it and exposes a service - Prevents
shared/utils.tsgod file
7. Test Use Case Without HTTP
import { describe, it, expect } from "node:test";
import { createOrder } from "../application/create-order";
describe("createOrder", () => {
it("saves valid order", async () => {
const saved: unknown[] = [];
const orders = { save: async (o: unknown) => { saved.push(o); }, findById: async () => null };
const result = await createOrder({ orders }, { customerId: "c1", sku: "SKU1", qty: 2 });
expect(result.status).toBe("pending");
expect(saved).toHaveLength(1);
});
});node:testruns in milliseconds- No
supertest, no port binding - Same pattern scales to Vitest/Jest
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.