整洁架构
clean-architecture
by giuseppe-trisciuoglio
梳理 NestJS/TypeScript 后端的 Clean Architecture、DDD 与 Ports & Adapters 实践,适合设计领域层、用例、端口适配器,或把臃肿 MVC 重构成可测试、低耦合架构。
安装
claude skill add --url github.com/giuseppe-trisciuoglio/developer-kit/tree/main/plugins/developer-kit-typescript/skills/clean-architecture文档
Clean Architecture, DDD & Hexagonal Architecture for NestJS
Overview
This skill provides comprehensive guidance for implementing Clean Architecture, Domain-Driven Design (DDD), and Hexagonal Architecture patterns in NestJS/TypeScript applications. It covers the architectural layers, tactical patterns, and practical implementation examples for building maintainable, testable, and loosely-coupled backend systems.
When to Use
- Architecting complex NestJS applications with long-term maintainability
- Refactoring from tightly-coupled MVC to layered architecture
- Implementing rich domain models with business logic encapsulation
- Designing testable systems with swappable infrastructure
- Creating microservices with clear bounded contexts
- Separating business rules from framework code
- Implementing event-driven architectures with domain events
Instructions
1. Understand the Architectural Layers
Clean Architecture organizes code into concentric layers where dependencies flow inward. Inner layers have no knowledge of outer layers:
+-------------------------------------+
| Infrastructure (Frameworks, DB) | Outer layer - volatile
+-------------------------------------+
| Adapters (Controllers, Repositories)| Interface adapters
+-------------------------------------+
| Application (Use Cases) | Business rules
+-------------------------------------+
| Domain (Entities, Value Objects) | Core - most stable
+-------------------------------------+
The Hexagonal Architecture (Ports & Adapters) pattern complements this:
- Ports: Interfaces defining what the application needs
- Adapters: Concrete implementations of ports
- Domain Core: Pure business logic with zero dependencies
2. Learn DDD Tactical Patterns
Apply these patterns in your domain layer:
- Entities: Objects with identity and lifecycle
- Value Objects: Immutable, defined by attributes
- Aggregates: Consistency boundaries with aggregate roots
- Domain Events: Capture state changes
- Repositories: Abstract data access for aggregates
3. Organize Your Project Structure
Structure your NestJS project following Clean Architecture principles:
src/
+-- domain/ # Inner layer - no external deps
| +-- entities/ # Domain entities
| +-- value-objects/ # Immutable value objects
| +-- aggregates/ # Aggregate roots
| +-- events/ # Domain events
| +-- repositories/ # Repository interfaces (ports)
| +-- services/ # Domain services
+-- application/ # Use cases - orchestration
| +-- use-cases/ # Individual use cases
| +-- ports/ # Input/output ports
| +-- dto/ # Application DTOs
| +-- services/ # Application services
+-- infrastructure/ # External concerns
| +-- database/ # ORM config, migrations
| +-- http/ # HTTP clients
| +-- messaging/ # Message queues
+-- adapters/ # Interface adapters
+-- http/ # Controllers, presenters
+-- persistence/ # Repository implementations
+-- external/ # External service adapters
4. Implement the Domain Layer
Create pure domain objects with no external dependencies:
- Value Objects: Immutable objects validated at construction
- Entities: Objects with identity containing business logic
- Aggregates: Consistency boundaries protecting invariants
- Repository Ports: Interfaces defining data access contracts
5. Implement the Application Layer
Create use cases that orchestrate business logic:
- Define input/output DTOs for each use case
- Inject repository ports via constructor
- Implement business workflows in the
executemethod - Keep use cases focused on a single responsibility
6. Implement Adapters
Create concrete implementations of ports:
- Persistence Adapters: Map domain objects to/from ORM entities
- HTTP Adapters: Controllers that transform requests to use case inputs
- External Service Adapters: Integrate with third-party services
7. Configure Dependency Injection
Wire everything together in NestJS modules:
- Register use cases as providers
- Provide repository implementations using interface tokens
- Import required infrastructure modules (TypeORM, etc.)
8. Apply Best Practices
Follow these principles throughout implementation:
- Dependency Rule: Dependencies only point inward. Domain knows nothing about NestJS, TypeORM, or HTTP.
- Rich Domain Models: Put business logic in entities, not services. Avoid anemic domain models.
- Immutability: Value objects must be immutable. Create new instances instead of modifying.
- Interface Segregation: Keep repository interfaces small and focused.
- Constructor Injection: Use NestJS DI in outer layers only. Domain entities use plain constructors.
- Validation: Validate at boundaries (DTOs) and enforce invariants in domain.
- Testing: Domain layer tests require no NestJS testing module - pure unit tests.
- Transactions: Keep transactions in the application layer, not domain.
Examples
Example 1: Value Objects
Value objects are immutable and validated at construction:
// domain/value-objects/email.vo.ts
export class Email {
private constructor(private readonly value: string) {}
static create(email: string): Email {
if (!this.isValid(email)) {
throw new Error('Invalid email format');
}
return new Email(email.toLowerCase().trim());
}
private static isValid(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
getValue(): string {
return this.value;
}
equals(other: Email): boolean {
return this.value === other.value;
}
}
// domain/value-objects/money.vo.ts
export class Money {
private constructor(
private readonly amount: number,
private readonly currency: string,
) {}
static create(amount: number, currency: string): Money {
if (amount < 0) throw new Error('Amount cannot be negative');
return new Money(amount, currency);
}
add(other: Money): Money {
if (this.currency !== other.currency) {
throw new Error('Cannot add different currencies');
}
return new Money(this.amount + other.amount, this.currency);
}
getAmount(): number { return this.amount; }
getCurrency(): string { return this.currency; }
}
Example 2: Entity with Business Logic
Entities contain identity and encapsulate business rules:
// domain/entities/order-item.entity.ts
import { Money } from '../value-objects/money.vo';
export class OrderItem {
constructor(
private readonly productId: string,
private readonly quantity: number,
private readonly unitPrice: Money,
) {
if (quantity <= 0) throw new Error('Quantity must be positive');
}
getSubtotal(): Money {
return Money.create(
this.unitPrice.getAmount() * this.quantity,
this.unitPrice.getCurrency(),
);
}
}
Example 3: Aggregate Root with Domain Events
Aggregate roots protect invariants and emit domain events:
// domain/aggregates/order.aggregate.ts
import { AggregateRoot } from '@nestjs/cqrs';
import { OrderItem } from '../entities/order-item.entity';
import { Money } from '../value-objects/money.vo';
import { OrderCreatedEvent } from '../events/order-created.event';
export enum OrderStatus {
PENDING = 'PENDING',
CONFIRMED = 'CONFIRMED',
SHIPPED = 'SHIPPED',
CANCELLED = 'CANCELLED',
}
export class Order extends AggregateRoot {
private items: OrderItem[] = [];
private status: OrderStatus = OrderStatus.PENDING;
constructor(
private readonly id: string,
private readonly customerId: string,
) {
super();
}
addItem(item: OrderItem): void {
if (this.status !== OrderStatus.PENDING) {
throw new Error('Cannot modify confirmed order');
}
this.items.push(item);
}
getTotal(): Money {
return this.items.reduce(
(sum, item) => sum.add(item.getSubtotal()),
Money.create(0, 'USD'),
);
}
confirm(): void {
if (this.items.length === 0) {
throw new Error('Cannot confirm empty order');
}
this.status = OrderStatus.CONFIRMED;
this.apply(new OrderCreatedEvent(this.id, this.customerId));
}
getStatus(): OrderStatus {
return this.status;
}
}
Example 4: Repository Port (Interface)
Define repository contracts in the domain layer:
// domain/repositories/order-repository.port.ts
import { Order } from '../aggregates/order.aggregate';
export interface OrderRepositoryPort {
findById(id: string): Promise<Order | null>;
findByCustomerId(customerId: string): Promise<Order[]>;
save(order: Order): Promise<void>;
delete(id: string): Promise<void>;
}
// Token for dependency injection
export const ORDER_REPOSITORY = Symbol('ORDER_REPOSITORY');
Example 5: Use Case (Application Layer)
Use cases orchestrate business logic and infrastructure:
// application/use-cases/create-order.use-case.ts
import { Injectable, Inject } from '@nestjs/common';
import { Order } from '../../domain/aggregates/order.aggregate';
import { OrderItem } from '../../domain/entities/order-item.entity';
import { Money } from '../../domain/value-objects/money.vo';
import { OrderRepositoryPort, ORDER_REPOSITORY } from '../../domain/repositories/order-repository.port';
export interface CreateOrderInput {
customerId: string;
items: Array<{
productId: string;
quantity: number;
unitPrice: number;
currency: string;
}>;
}
export interface CreateOrderOutput {
orderId: string;
total: number;
currency: string;
}
@Injectable()
export class CreateOrderUseCase {
constructor(
@Inject(ORDER_REPOSITORY)
private readonly orderRepository: OrderRepositoryPort,
) {}
async execute(input: CreateOrderInput): Promise<CreateOrderOutput> {
const orderId = crypto.randomUUID();
const order = new Order(orderId, input.customerId);
for (const item of input.items) {
const money = Money.create(item.unitPrice, item.currency);
order.addItem(new OrderItem(item.productId, item.quantity, money));
}
order.confirm();
await this.orderRepository.save(order);
const total = order.getTotal();
return {
orderId,
total: total.getAmount(),
currency: total.getCurrency(),
};
}
}
Example 6: Repository Adapter (Infrastructure)
Implement repository interfaces in the infrastructure layer:
// adapters/persistence/order.repository.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { OrderRepositoryPort } from '../../domain/repositories/order-repository.port';
import { Order } from '../../domain/aggregates/order.aggregate';
import { OrderEntity } from '../../infrastructure/database/entities/order.entity';
@Injectable()
export class OrderRepository implements OrderRepositoryPort {
constructor(
@InjectRepository(OrderEntity)
private readonly repository: Repository<OrderEntity>,
) {}
async findById(id: string): Promise<Order | null> {
const entity = await this.repository.findOne({
where: { id },
relations: ['items'],
});
return entity ? this.toDomain(entity) : null;
}
async findByCustomerId(customerId: string): Promise<Order[]> {
const entities = await this.repository.find({
where: { customerId },
relations: ['items'],
});
return entities.map(e => this.toDomain(e));
}
async save(order: Order): Promise<void> {
const entity = this.toEntity(order);
await this.repository.save(entity);
}
async delete(id: string): Promise<void> {
await this.repository.delete(id);
}
private toDomain(entity: OrderEntity): Order {
// Map ORM entity to domain aggregate
const order = new Order(entity.id, entity.customerId);
// ... populate items, status
return order;
}
private toEntity(order: Order): OrderEntity {
// Map domain aggregate to ORM entity
const entity = new OrderEntity();
// ... mapping logic
return entity;
}
}
Example 7: Controller Adapter
Controllers adapt HTTP requests to use case inputs:
// adapters/http/order.controller.ts
import { Controller, Post, Body, Get, Param } from '@nestjs/common';
import { CreateOrderUseCase, CreateOrderInput } from '../../application/use-cases/create-order.use-case';
import { IsString, IsArray, ValidateNested, IsNumber } from 'class-validator';
import { Type } from 'class-transformer';
class OrderItemDto {
@IsString()
productId: string;
@IsNumber()
quantity: number;
@IsNumber()
unitPrice: number;
@IsString()
currency: string;
}
class CreateOrderDto implements CreateOrderInput {
@IsString()
customerId: string;
@IsArray()
@ValidateNested({ each: true })
@Type(() => OrderItemDto)
items: OrderItemDto[];
}
@Controller('orders')
export class OrderController {
constructor(private readonly createOrderUseCase: CreateOrderUseCase) {}
@Post()
async create(@Body() dto: CreateOrderDto) {
return this.createOrderUseCase.execute(dto);
}
}
Example 8: Module Configuration
Wire dependencies together in NestJS modules:
// orders.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { OrderController } from './adapters/http/order.controller';
import { CreateOrderUseCase } from './application/use-cases/create-order.use-case';
import { OrderRepository } from './adapters/persistence/order.repository';
import { ORDER_REPOSITORY } from './domain/repositories/order-repository.port';
import { OrderEntity } from './infrastructure/database/entities/order.entity';
@Module({
imports: [TypeOrmModule.forFeature([OrderEntity])],
controllers: [OrderController],
providers: [
CreateOrderUseCase,
{
provide: ORDER_REPOSITORY,
useClass: OrderRepository,
},
],
})
export class OrdersModule {}
Best Practices
- Dependency Rule: Dependencies only point inward - domain knows nothing about NestJS, TypeORM, or HTTP
- Rich Domain Models: Put business logic in entities, not services - avoid anemic domain models
- Immutability: Value objects must be immutable - use private constructors with static factory methods
- Interface Segregation: Keep repository interfaces small and focused - one repository per aggregate
- Constructor Injection: Use NestJS DI in outer layers only - domain entities use plain constructors
- Validation at Boundaries: Validate DTOs at API boundary and enforce invariants in domain entities
- Pure Domain Tests: Domain layer tests require no NestJS testing module - fast pure unit tests
- Transactions in Application: Keep transaction management in application layer, not domain
- Symbol Tokens: Use Symbol() for DI tokens to avoid string coupling in NestJS modules
- Aggregate Roots: Protect invariants through aggregate roots - access entities only through aggregates
Constraints and Warnings
Architecture Constraints
- Dependency Rule: Never allow inner layers to depend on outer layers
- Domain Purity: Domain layer must have zero dependencies on frameworks (NestJS, TypeORM, etc.)
- Interface Location: Repository interfaces belong in the domain layer, implementations in adapters
- Immutability: Value objects must be immutable - no setters allowed
Common Pitfalls to Avoid
- Leaky Abstractions: ORM entities leaking into domain layer
- Anemic Domain: Entities with only getters/setters, logic in services
- Wrong Layer: Framework decorators in domain entities
- Missing Ports: Direct dependency on concrete implementations instead of interfaces
- Over-Engineering: Clean Architecture for simple CRUD operations is unnecessary overhead
Implementation Warnings
- Mapping Overhead: Repository adapters require mapping between domain and ORM entities
- Learning Curve: Team must understand DDD concepts before implementation
- Boilerplate: More files and interfaces compared to traditional layered architecture
- Transaction Boundaries: Transactions must be managed at the application layer, not domain
References
references/typescript-clean-architecture.md- TypeScript-specific patternsreferences/nestjs-implementation.md- NestJS integration details
相关 Skills
网页构建器
by anthropics
面向复杂 claude.ai HTML artifact 开发,快速初始化 React + Tailwind CSS + shadcn/ui 项目并打包为单文件 HTML,适合需要状态管理、路由或多组件交互的页面。
✎ 在 claude.ai 里做复杂网页 Artifact 很省心,多组件、状态和路由都能顺手搭起来,React、Tailwind 与 shadcn/ui 组合效率高、成品也更精致。
前端设计
by anthropics
面向组件、页面、海报和 Web 应用开发,按鲜明视觉方向生成可直接落地的前端代码与高质感 UI,适合做 landing page、Dashboard 或美化现有界面,避开千篇一律的 AI 审美。
✎ 想把页面做得既能上线又有设计感,就用前端设计:组件到整站都能产出,难得的是能避开千篇一律的 AI 味。
网页应用测试
by anthropics
用 Playwright 为本地 Web 应用编写自动化测试,支持启动开发服务器、校验前端交互、排查 UI 异常、抓取截图与浏览器日志,适合调试动态页面和回归验证。
✎ 借助 Playwright 一站式验证本地 Web 应用前端功能,调 UI 时还能同步查看日志和截图,定位问题更快。
相关 MCP 服务
GitHub
编辑精选by GitHub
GitHub 是 MCP 官方参考服务器,让 Claude 直接读写你的代码仓库和 Issues。
✎ 这个参考服务器解决了开发者想让 AI 安全访问 GitHub 数据的问题,适合需要自动化代码审查或 Issue 管理的团队。但注意它只是参考实现,生产环境得自己加固安全。
Context7 文档查询
编辑精选by Context7
Context7 是实时拉取最新文档和代码示例的智能助手,让你告别过时资料。
✎ 它能解决开发者查找文档时信息滞后的问题,特别适合快速上手新库或跟进更新。不过,依赖外部源可能导致偶尔的数据延迟,建议结合官方文档使用。
by tldraw
tldraw 是让 AI 助手直接在无限画布上绘图和协作的 MCP 服务器。
✎ 这解决了 AI 只能输出文本、无法视觉化协作的痛点——想象让 Claude 帮你画流程图或白板讨论。最适合需要快速原型设计或头脑风暴的开发者。不过,目前它只是个基础连接器,你得自己搭建画布应用才能发挥全部潜力。