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);

API 文档界面

性能优化

优化项方法
数据库连接连接池配置
查询优化只查需要的字段
缓存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 的模块化设计很好,但初学有点绕。关键是理解依赖注入和模块系统。

项目结构没有标准答案,适合团队的就是最好的。这套架构我们已经用了两年,稳定可靠。