Clean Architecture for NestJS
Reference: Uncle Bob's Clean Architecture (Entities -> Use Cases -> Interface Adapters -> Frameworks & Drivers). Dependency rule points inward.
1. Layer map
| Circle |
Name |
NestJS location |
Contains |
| Yellow center |
Enterprise Business Rules |
src/domain/ |
Entities, Value Objects, Aggregates, Domain Events, Repository interfaces |
| Red |
Application Business Rules |
src/application/ |
Use Cases (Interactors), Input/Output Ports, Application DTOs |
| Green |
Interface Adapters |
src/interface/, src/application/ |
Controllers, Presenters, Gateways, Mappers |
| Blue |
Frameworks & Drivers |
src/infrastructure/, src/main.ts |
NestJS modules, TypeORM/Prisma, HTTP server, DB, external APIs |
Flow of control: Controller -> Input Port -> UseCase Interactor -> Output Port -> Presenter.
2. Allowed imports
interface/controllers -> application/ports/in, application/dtos
application/use-cases -> domain/*, application/ports/out
infrastructure/* -> application/ports/*, domain/*
domain/* -> nothing outside domain (std lib + domain errors only)
Forbidden: domain importing typeorm, @nestjs/*, prisma; application importing infrastructure; controller containing business if.
3. Canonical NestJS example
// src/domain/entities/order.ts - no decorators
export class Order { /* invariants + methods */ }
// src/application/ports/in/create-order.input-port.ts
export interface CreateOrderInputPort { execute(input: CreateOrderInput): Promise<CreateOrderOutput>; }
// src/application/use-cases/create-order.use-case.ts
@Injectable()
export class CreateOrderUseCase implements CreateOrderInputPort {
constructor(@Inject(ORDER_REPOSITORY) private readonly orders: OrderRepository) {}
async execute(input: CreateOrderInput) { /* orchestrate */ }
}
// src/interface/controllers/order.controller.ts - thin
@Controller('orders')
export class OrderController {
constructor(@Inject(CreateOrderUseCase) private readonly createOrder: CreateOrderInputPort) {}
@Post() create(@Body() dto: CreateOrderDto) { return this.createOrder.execute(dto); }
}
// src/infrastructure/di/order.module.ts
@Module({
controllers: [OrderController],
providers: [
CreateOrderUseCase,
{ provide: ORDER_REPOSITORY, useClass: TypeOrmOrderRepository },
],
})
export class OrderModule {}
4. Anti-patterns to fix
- Anemic TypeORM
@Entity() used as domain entity -> split into Order (domain) + OrderOrmEntity (infra) + mapper.
- Business logic in
@Controller() -> move to UseCase.
- UseCase doing
new PrismaClient() or injectRepository() directly -> depend on OrderRepository port.
- Presenter returning ORM object -> map to view model DTO.
5. Checklist before done
1---2name: clean-architecture-nestjs3description: Use when scaffolding NestJS modules, controllers, use-cases, or checking Clean Architecture dependency direction.4---56# Clean Architecture for NestJS78Reference: Uncle Bob's Clean Architecture (Entities -> Use Cases -> Interface Adapters -> Frameworks & Drivers). Dependency rule points inward.910## 1. Layer map1112| Circle | Name | NestJS location | Contains |13|--------|------|-----------------|----------|14| Yellow center | Enterprise Business Rules | `src/domain/` | Entities, Value Objects, Aggregates, Domain Events, Repository interfaces |15| Red | Application Business Rules | `src/application/` | Use Cases (Interactors), Input/Output Ports, Application DTOs |16| Green | Interface Adapters | `src/interface/`, `src/application/` | Controllers, Presenters, Gateways, Mappers |17| Blue | Frameworks & Drivers | `src/infrastructure/`, `src/main.ts` | NestJS modules, TypeORM/Prisma, HTTP server, DB, external APIs |1819Flow of control: `Controller -> Input Port -> UseCase Interactor -> Output Port -> Presenter`.2021## 2. Allowed imports2223```24interface/controllers -> application/ports/in, application/dtos25application/use-cases -> domain/*, application/ports/out26infrastructure/* -> application/ports/*, domain/*27domain/* -> nothing outside domain (std lib + domain errors only)28```2930Forbidden: `domain` importing `typeorm`, `@nestjs/*`, `prisma`; `application` importing `infrastructure`; controller containing business `if`.3132## 3. Canonical NestJS example3334```ts35// src/domain/entities/order.ts - no decorators36export class Order { /* invariants + methods */ }3738// src/application/ports/in/create-order.input-port.ts39export interface CreateOrderInputPort { execute(input: CreateOrderInput): Promise<CreateOrderOutput>; }4041// src/application/use-cases/create-order.use-case.ts42@Injectable()43export class CreateOrderUseCase implements CreateOrderInputPort {44 constructor(@Inject(ORDER_REPOSITORY) private readonly orders: OrderRepository) {}45 async execute(input: CreateOrderInput) { /* orchestrate */ }46}4748// src/interface/controllers/order.controller.ts - thin49@Controller('orders')50export class OrderController {51 constructor(@Inject(CreateOrderUseCase) private readonly createOrder: CreateOrderInputPort) {}52 @Post() create(@Body() dto: CreateOrderDto) { return this.createOrder.execute(dto); }53}5455// src/infrastructure/di/order.module.ts56@Module({57 controllers: [OrderController],58 providers: [59 CreateOrderUseCase,60 { provide: ORDER_REPOSITORY, useClass: TypeOrmOrderRepository },61 ],62})63export class OrderModule {}64```6566## 4. Anti-patterns to fix6768- Anemic TypeORM `@Entity()` used as domain entity -> split into `Order` (domain) + `OrderOrmEntity` (infra) + mapper.69- Business logic in `@Controller()` -> move to UseCase.70- UseCase doing `new PrismaClient()` or `injectRepository()` directly -> depend on `OrderRepository` port.71- Presenter returning ORM object -> map to view model DTO.7273## 5. Checklist before done7475- [ ] `domain/` has zero `@nestjs/*` / `typeorm` imports (`grep -r "@nestjs\|typeorm\|prisma" src/domain` empty).76- [ ] Each use case has explicit Input + Output Port.77- [ ] Controller depends on Input Port token, not concrete class.78- [ ] Module wires ports via `provide/useClass`.79- [ ] Use case unit-testable with mocked outbound ports.