NestJS 项目架构设计
用 NestJS 做了三个项目,踩了不少坑,沉淀出一套架构模式。
目录结构
src/
├── modules/ # 业务模块
│ ├── user/
│ │ ├── user.module.ts
│ │ ├── user.controller.ts
│ │ ├── user.service.ts
│ │ ├── user.entity.ts
│ │ ├── dto/
│ │ └── interfaces/
│ ├── auth/
│ └── post/
├── common/ # 公共模块
│ ├── decorators/
│ ├── filters/
│ ├── guards/
│ ├── interceptors/
│ ├── pipes/
│ └── utils/
├── config/ # 配置
│ ├── database.config.ts
│ ├── redis.config.ts
│ └── index.ts
├── database/ # 数据库
│ ├── migrations/
│ └── seeds/
└── main.ts
模块划分原则
| 原则 | 说明 |
|---|---|
| 高内聚 | 模块内部功能紧密相关 |
| 低耦合 | 模块间依赖最小化 |
| 单一职责 | 一个模块只做一件事 |
| 可复用 | 通用逻辑抽到 common |
依赖注入
NestJS 的核心是 DI,用好它很重要:
// user.service.ts
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
private readonly cacheService: CacheService,
) {}
async findOne(id: number): Promise<User | null> {
const cacheKey = `user:${id}`;
const cached = await this.cacheService.get(cacheKey);
if (cached) return cached;
const user = await this.userRepository.findOne({ where: { id } });
if (user) {
await this.cacheService.set(cacheKey, user, 3600);
}
return user;
}
}
全局异常处理
// common/filters/all-exceptions.filter.ts
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException
? exception.getResponse()
: '服务器内部错误';
response.status(status).json({
code: status,
timestamp: new Date().toISOString(),
path: request.url,
message,
});
}
}
// main.ts
app.useGlobalFilters(new AllExceptionsFilter());
响应拦截器
统一返回格式:
// common/interceptors/transform.interceptor.ts
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<Response<T>> {
return next.handle().pipe(
map((data) => ({
code: 200,
message: 'success',
data,
})),
);
}
}
interface Response<T> {
code: number;
message: string;
data: T;
}
自定义装饰器
// common/decorators/user.decorator.ts
export const CurrentUser = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] : user;
},
);
// 使用
@Get('profile')
getProfile(@CurrentUser() user: User) {
return user;
}
数据库事务
// common/decorators/transactional.decorator.ts
export function Transactional() {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor,
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const dataSource = this.dataSource || getDataSource();
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const result = await originalMethod.apply(this, [
...args,
queryRunner.manager,
]);
await queryRunner.commitTransaction();
return result;
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
};
};
}
配置管理
// config/index.ts
import { ConfigModule, ConfigService } from '@nestjs/config';
export default ConfigModule.forRoot({
isGlobal: true,
envFilePath: [`.env.${process.env.NODE_ENV}`, '.env'],
load: [databaseConfig, redisConfig],
});
// 使用
constructor(private configService: ConfigService) {
const dbHost = this.configService.get<string>('database.host');
}
定时任务
import { Cron, CronExpression } from '@nestjs/schedule';
@Injectable()
export class TaskService {
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
async cleanExpiredTokens() {
await this.tokenRepository.delete({
expiresAt: LessThan(new Date()),
});
}
}
模块间通信
事件驱动
// events/user-created.event.ts
export class UserCreatedEvent {
constructor(
public readonly userId: number,
public readonly email: string,
) {}
}
// user.service.ts
this.eventEmitter.emit('user.created', new UserCreatedEvent(user.id, user.email));
// notification.listener.ts
@OnEvent('user.created')
async handleUserCreated(event: UserCreatedEvent) {
await this.sendWelcomeEmail(event.email);
}
API 文档
// main.ts
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
const config = new DocumentBuilder()
.setTitle('API 文档')
.setDescription('项目 API 接口文档')
.setVersion('1.0')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api-docs', app, document);
性能优化
| 优化项 | 方法 |
|---|---|
| 数据库连接 | 连接池配置 |
| 查询优化 | 只查需要的字段 |
| 缓存 | Redis 缓存热点数据 |
| 压缩 | gzip 中间件 |
| 并发 | 集群模式 |
测试
// user.service.spec.ts
describe('UserService', () => {
let service: UserService;
let repository: MockType<Repository<User>>;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
UserService,
{
provide: getRepositoryToken(User),
useFactory: repositoryMockFactory,
},
],
}).compile();
service = module.get(UserService);
repository = module.get(getRepositoryToken(User));
});
it('should find a user', async () => {
repository.findOne.mockReturnValue({ id: 1, name: 'test' });
const result = await service.findOne(1);
expect(result).toBeDefined();
});
});
总结
NestJS 的模块化设计很好,但初学有点绕。关键是理解依赖注入和模块系统。
项目结构没有标准答案,适合团队的就是最好的。这套架构我们已经用了两年,稳定可靠。