Module anatomy
Every feature module follows the same internal shape. Consistency here is worth
more than local cleverness — a developer opening orders/ for the first time
should already know where everything is.
<module>/
├── <module>.module.ts # Wiring only. No logic, ever.
├── <module>.controller.ts # HTTP surface: parse, delegate, return. No rules.
├── <module>.service.ts # Business rules. The only interesting file.
├── <module>.repository.ts # The ONLY file allowed to touch PrismaService.
├── dto/ # Request/response shapes + Zod schema bindings.
├── mappers/ # Prisma row → API type. Keeps Prisma types internal.
├── events/ # Events this module publishes and subscribes to.
└── public/
└── index.ts # The only entry point for other modules.
The four rules
-
Controllers contain no business logic. If a controller has an
ifthat is not input shaping, the rule belongs in the service. -
Only the repository imports Prisma. Services depend on repository interfaces. This is what makes services unit-testable without a database and what keeps a later storage change from rippling outward.
-
A module owns its tables exclusively.
OrdersModulenever queriesproducts— it asksProductsModule's public service, or it stores a snapshot. Shared tables are how a monolith becomes unsplittable. -
Cross-module imports go through
public/. Deep imports are blocked by ESLint (@sport/eslint-config/nest). If you need something that is not exported, widen the public surface deliberately — do not reach around it.
Talking to another module
| Need | Mechanism |
|---|---|
| An answer, now, to continue this request | Call its public service |
| To react to something that happened | Subscribe to its domain event |
| To change its data | Call its public service — never write its tables |
Modules marked EXTRACTION CANDIDATE
inventory, orders, payments and search are written so they could become
independent services later: no foreign reads, communication via events, and no
shared transactions with the rest of the monolith beyond their own tables.
That is a constraint on how they are written, not a plan to extract them. Extraction is justified by a real scaling or team-boundary problem, and nothing here assumes it will ever happen.