Modularity Best Practices
Import direction: routes → use-cases → domain. These practices keep Node.js codebases testable and ready for framework or persistence changes.
How to Use This List
- Automate import rules before code review debates
- Apply to workers and CLI entry points, not only HTTP
- Pair with Architecture Best Practices for macro boundaries
- New routes must call existing use cases - no duplicate orchestration
A - Layer Discipline
- Route handlers stay under 15 lines. Parse, authorize, call use case, map response.
- Use cases own orchestration and application rules. Not repositories, not routes.
- Domain layer has zero imports from
express,fastify,@nestjs/*,prisma,ioredis. - Infrastructure implements ports defined in domain/application. Map ORM rows to domain entities in adapter.
- Composition root is the only place that instantiates concrete adapters.
B - Import Direction
- Allowed flow:
infrastructure→application→domain. Never domain → infrastructure. - Cross-feature calls go through module public API or application service. No reaching into peer
infrastructure/. - CI runs
dependency-cruiseror ESLint to fail forbidden imports. - Shared kernel contains utilities only. No pricing, tax, or discount rules in
shared/. - Barrel files export intentional public surface. No
export * from './infrastructure'.
C - Testing
- Unit test use cases with in-memory repositories. Sub-10ms tests per command.
- Integration tests hit real Postgres via Testcontainers for repository adapters.
- HTTP tests (supertest) cover status codes and auth. Not every business rule branch.
- Workers call the same use cases as HTTP routes. No forked business logic.
- Reset DI container or config cache between tests.
D - Use Case Design
- One use case per command or query. No
OrderGodService. - Explicit input/output types. Adapters map to JSON DTOs.
- Authorization checks live in use case or domain policy. Not only route middleware.
- Idempotency and transactions documented per use case.
- Return domain errors typed (
AppError). Central HTTP mapper translates codes.
E - Persistence
- Repository interfaces express domain language.
findById, notfindUnique. - No Prisma client in use cases. Inject
OrderRepository. - Heavy reporting uses read models or query services. Not bloated repositories.
- In-memory repo shipped alongside Postgres adapter for fast tests.
- Pagination cursors returned from repository port when lists are large.
F - Framework Notes (Express 5 / Fastify 5 / Nest 11)
- Express/Fastify: factory routers
ordersRouter(deps)for injection. - Fastify: use plugins with encapsulated scope when modules need isolation.
- Nest: controllers thin; providers map to use cases. Avoid fat
OrdersServicegod class. - Validate
req.bodywith Zod at HTTP edge beforeexecute(). - One HTTP framework per deployable. Do not mix Express and Fastify in one process.
FAQs
What is the minimum structure for a 5-route API?
routes.ts, use-cases/, optional repos/ is enough. Full hexagonal folders when second adapter appears.
Can middleware call use cases?
Auth middleware can attach user to request. Business authorization still in use case.
How do I migrate fat routes incrementally?
Extract one use case per PR. Route calls new use case; delete old inline logic when tests pass.
Are DTOs required?
Explicit types at use case boundary help. HTTP DTOs can differ from domain entities via mapper functions.
Functional style without classes?
execute(deps, input) functions work. Keep same import rules.
Top modularity smell?
import { prisma } from '../lib/prisma' inside route handlers.
Monorepo shared database package?
Schema package OK; each service owns repository adapter - no shared Prisma calls from routes.
GraphQL resolvers?
Resolvers are adapters - call use cases, do not embed SQL.
How strict for scripts folder?
scripts/ can be loose. src/ production code follows this list.
Does this block shipping?
No for week-one MVP if tech-debt ticket tracks extraction before squad #2.
Related
- Modularity Basics - introductory examples
- Use Cases & Services - application layer
- Repository Pattern - persistence ports
- Dependency Injection Patterns - wiring
- Refactoring Checklist - structural audit
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.