NestJS 是 Node.js 生态中最具框架感的后端解决方案,受 Angular 装饰器模式的启发,原生基于 TypeScript 设计,提供了模块化架构、依赖注入容器、面向切面编程(AOP)等工程化能力。它并非 Express 的替代品,而是构建于 Express(或 Fastify)之上的元框架,通过应用层抽象将企业级开发模式引入 Node.js。
1. Module 封装边界与模块关系图
NestJS 中的 Module 不仅是一个代码组织单位,更是依赖注入容器的边界。每个模块通过 @Module() 装饰器声明其内部的 controllers、providers、imports 和 exports。imports 引入了其他模块中 exports 的 provider,使得跨模块依赖注入成为可能;未被导出的 provider 则严格封装在模块内部,外部模块无法访问。
// users/users.module.ts
@Module({
controllers: [UsersController],
providers: [UsersService, UsersRepository],
imports: [TypeOrmModule.forFeature([UserEntity])],
exports: [UsersService], // 仅导出 UsersService,UsersRepository 保持私有
})
export class UsersModule {}
// orders/orders.module.ts
@Module({
controllers: [OrdersController],
providers: [OrdersService],
imports: [UsersModule], // 通过导入获得 UsersService 的注入能力
})
export class OrdersModule {}
全局模块通过 @Global() 装饰器注册,其导出的 provider 可被任意模块消费,无需显式导入。这种方式减少了深层嵌套依赖中的重复 imports,但应谨慎使用,过多全局模块会破坏模块边界的隔离性,导致模块间的隐式耦合增加。理想状态下,全局模块只应包含基础设施级别的 provider:配置服务、日志服务、数据库连接池等通用横切关注点。
@Global()
@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule {}
动态模块允许模块在导入时根据运行时参数配置 provider。这是实现数据库连接、缓存配置等可复用基础设施模块的核心机制。
@Module({})
export class DatabaseModule {
static forRoot(options: DatabaseOptions): DynamicModule {
return {
module: DatabaseModule,
providers: [
{
provide: 'DATABASE_OPTIONS',
useValue: options,
},
ConnectionFactory,
],
exports: [ConnectionFactory],
};
}
}
2. Provider 生命周期:Singleton / Request / Transient
NestJS 依赖注入容器内置三种作用域控制,决定了 provider 实例的创建与销毁时机。
Singleton(默认):在应用启动时创建单个实例,并在整个应用生命周期中复用。适用于无状态服务、工具类、数据访问对象。Singleton 的内存占用最低,但绝不应该在其中存储请求级状态,否则会导致跨请求的数据污染。
Request:每个 HTTP 请求进入 NestJS 时,容器会为该请求创建独立的子注入器及一组专属实例。当请求结束、响应发送完毕后,Request 作用域的 provider 会被垃圾回收。这种作用域适用于需要追踪请求级上下文的场景,如多租户数据库连接切换、请求链路追踪器。
Transient:每次注入时都会生成新的实例。即使同一请求中多个 provider 依赖同一个 transient provider,每个注入点都会获得独立实例。开销最大,仅适用于极短生命周期的有状态对象。
@Injectable({ scope: Scope.REQUEST })
export class TenantConnectionService {
private connection: DataSource;
constructor(@Inject(REQUEST) private request: Request) {
const tenantId = request.headers['x-tenant-id'] as string;
this.connection = createConnectionForTenant(tenantId);
}
getRepository<T>(entity: EntityTarget<T>): Repository<T> {
return this.connection.getRepository(entity);
}
}
// 警告:注入 Request 作用域 provider 的控制器/服务
// 会自动继承 REQUEST 作用域,波及整个依赖树
@Controller('orders')
export class OrdersController {
constructor(
private ordersService: OrdersService, // 若依赖了 TenantConnectionService,会自动变为 REQUEST 作用域
private tenantConn: TenantConnectionService,
) {}
}
Request 作用域带来的性能损耗不可忽视:每个请求都需重新实例化依赖链上的所有 provider。应通过 AsyncLocalStorage 或自定义装饰器在 Singleton 中安全访问请求上下文,以减少作用域蔓延。例如可以将请求级别的 tenant ID 存入 AsyncLocalStorage,而数据库连接池本身保持 Singleton,仅在查询时切换 search_path 或 schema。
const als = new AsyncLocalStorage<{ tenantId: string }>();
@Injectable()
export class TenantContextService {
setTenant(tenantId: string): void {
const store = als.getStore();
if (store) store.tenantId = tenantId;
}
getTenant(): string | undefined {
return als.getStore()?.tenantId;
}
}
// 在中间件中初始化上下文
app.use((req, res, next) => {
const tenantId = req.headers['x-tenant-id'] as string;
als.run({ tenantId }, () => next());
});
这种方式让依赖树保持在 Singleton 作用域,每个请求的上下文通过异步上下文传递,性能开销远低于完整的 Request 作用域实例化。
3. 自定义装饰器与 reflect-metadata
NestJS 的装饰器系统建立在 reflect-metadata 之上。@Injectable()、@Controller()、@Get() 等内置装饰器在编译阶段通过 Reflect.metadata 将元数据挂载到类和属性上,运行时 NestJS 通过 Reflect.getMetadata 读取这些信息,完成路由注册和依赖解析。
自定义装饰器可显著降低重复样板代码。以下示例实现了一个基于角色的权限装饰器,将角色信息写入元数据,供 Guard 读取。
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
// 使用方式
@Controller('admin')
export class AdminController {
@Post('users')
@Roles('super_admin', 'user_manager')
createUser() {
return { created: true };
}
}
// 对应的 Guard 实现
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some(role => user.roles?.includes(role));
}
}
组合装饰器(Composition)能进一步简化控制器声明。通过 applyDecorators 将多个常用装饰器打包为一个语义化装饰器。
import { applyDecorators, UseGuards, UseInterceptors } from '@nestjs/common';
export function Auth(...roles: string[]) {
return applyDecorators(
Roles(...roles),
UseGuards(JwtAuthGuard, RolesGuard),
UseInterceptors(LoggingInterceptor),
);
}
// 使用方式
@Controller('reports')
export class ReportsController {
@Get('monthly')
@Auth('finance_reader', 'admin')
getMonthlyReport() {
return this.reportsService.generateMonthly();
}
}
4. Guards、Interceptors 与 Pipes 的执行顺序
在 NestJS 的请求处理管道中,三类组件承担不同职责,且以严格的顺序调用。理解它们的执行位置有助于准确定位日志记录、权限校验和数据验证的切入点。实际开发中一个常见的困惑是:为什么 Guards 先于 Interceptors 执行?这是因为 Guards 负责准入控制(CanActivate),如果请求未被授权,它应该在任何业务逻辑(包括拦截器中的前置逻辑,如审计日志或参数解密)运行之前被拒绝。Interceptors 则更多用于横切关注点(Cross-cutting Concerns),如响应转换、缓存和超时控制,这些逻辑在权限已确认后是安全的。
执行顺序如下:
- Middleware(全局/模块级):与 Express 中间件等价,最早介入,可操作原始
req/res对象。 - Guards:在路由处理之前执行,返回值
true/false决定是否允许请求继续。负责对身份、权限、IP 白名单等进行准入控制。 - Interceptors(before):在路由处理之前执行的拦截器
intercept方法前半段。 - Pipes:在路由参数绑定阶段执行,负责数据转换(Transformation)和验证(Validation)。
ValidationPipe是最常用的内置管道。 - Controller / Provider:业务逻辑主体。
- Interceptors(after):拦截器的后半段,可修改返回的响应数据。
- Exception Filters:捕获全局或局部抛出的异常,格式化错误响应。
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
const now = Date.now();
return next.handle().pipe(
map(data => ({
data,
timestamp: new Date().toISOString(),
duration: `${Date.now() - now}ms`,
})),
);
}
}
// 全局注册
declare module '@nestjs/common' {
interface Response<T> {
data: T;
timestamp: string;
duration: string;
}
}
app.useGlobalInterceptors(new TransformInterceptor());
异常过滤器通过 @Catch() 声明其捕获的异常类型,可完全接管 NestJS 的默认异常响应格式。ArgumentsHost 不仅支持 HTTP 上下文,还支持 WebSocket 和 RPC 上下文,这意味着同一个 Exception Filter 可以在三种通信协议之间复用,只需通过 host.getType() 判断当前上下文类型并切换响应方式。
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: exception.message,
});
}
}
5. GraphQL Code First vs Schema First
NestJS 支持两种 GraphQL 开发范式。Schema First 要求开发者手写 .graphql 文件,通过代码生成 TypeScript 类型;Code First 则基于 TypeScript 的装饰器自动生成 SDL(Schema Definition Language)。
Schema First 更适合已有 GraphQL 团队规范、需要精确控制 schema 文本的场景。NestJS 通过 @nestjs/graphql 的 forRoot 配置 typePaths 和 resolvers 来加载手写 schema。
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
typePaths: ['./**/*.graphql'],
resolvers: { DateTime: GraphQLDateTime },
})
Code First 更符合 NestJS 装饰器驱动的心智模型:开发者使用 @ObjectType()、@Field() 和 @Resolver() 定义类型和解析器,NestJS 在启动时自动生成可执行的 schema。这种方式减少了 schema 与类型定义之间的重复,利用 TypeScript 编译器确保模型与 API 的一致性。
@ObjectType()
export class User {
@Field(() => ID)
id: string;
@Field()
email: string;
@Field({ nullable: true })
displayName?: string;
@Field(() => [Post])
posts: Post[];
}
@Resolver(() => User)
export class UsersResolver {
constructor(private usersService: UsersService) {}
@Query(() => User)
async user(@Args('id') id: string): Promise<User> {
return this.usersService.findById(id);
}
@ResolveField(() => [Post])
async posts(@Parent() user: User): Promise<Post[]> {
return this.usersService.findPostsByUserId(user.id);
}
}
Code First 模式下,@ResolveField 用于解析对象类型的嵌套字段,自动实现 N+1 查询的批处理需要结合 DataLoader。NestJS 社区提供 nestjs-dataloader 包将 DataLoader 实例的生命周期与请求作用域绑定。
两种范式的选择没有绝对标准。Schema First 在前后端分离且已有 GraphQL Schema 为标准合同(contract)的团队中表现更好,后端严格遵循 Schema 定义,前端根据 Schema 自动生成类型安全的查询客户端。Code First 则更适合后端驱动 API 演进的场景,开发者可以用单一语言(TypeScript)维护类型系统和业务逻辑,避免 schema 文件与 TS 接口之间的二义性。但 Code First 的隐式 schema 生成可能在复杂类型联合(Union Types)和接口继承场景中生成出乎意料的 SDL,需要额外注意 @InterfaceType() 和 @UnionType() 的用法。
6. 微服务模式:TCP、gRPC 与 RabbitMQ
NestJS 的微服务封装层将底层传输协议抽象为统一的 ClientProxy 和 @MessagePattern() / @EventPattern() 接口,开发者只需关注业务消息格式,而非 Socket 或 AMQP 的底层细节。这种抽象屏蔽了不同传输协议之间的差异,但也意味着开发者必须理解每种传输的语义限制:TCP 微服务不支持原生的事件广播(fire and forget 需要特殊配置),RabbitMQ 的 request/response 模式依赖回复队列的自动管理,gRPC 的流式 RPC 需要使用 RxJS Observable 进行适配。
6.1 TCP 传输
TCP 是 NestJS 默认的微服务传输方式,适合服务间处于同一私有网络、对通信协议无强制要求的场景。启动时创建 TCP 服务端监听指定端口,客户端通过 ClientsModule 配置连接信息。
// 服务端
@Controller()
export class MathController {
@MessagePattern({ cmd: 'sum' })
sum(data: number[]): number {
return data.reduce((a, b) => a + b, 0);
}
}
const microserviceOptions: TcpOptions = {
transport: Transport.TCP,
options: { host: '0.0.0.0', port: 8877 },
};
const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, microserviceOptions);
await app.listen();
// 客户端
@Module({
imports: [
ClientsModule.register([
{
name: 'MATH_SERVICE',
transport: Transport.TCP,
options: { host: 'math-service', port: 8877 },
},
]),
],
})
export class AppModule {}
@Injectable()
export class AppService {
constructor(@Inject('MATH_SERVICE') private mathClient: ClientProxy) {}
async accumulate(data: number[]): Promise<number> {
return lastValueFrom(this.mathClient.send<number>({ cmd: 'sum' }, data));
}
}
6.2 gRPC 传输
gRPC 基于 HTTP/2 和 Protocol Buffers,具备强类型接口定义、双向流通信和原生负载均衡能力。NestJS 的 gRPC 微服务需要手写 .proto 文件(Schema First),然后由 @grpc/proto-loader 动态加载。
syntax = "proto3";
package hero;
service HeroService {
rpc FindOne (HeroById) returns (Hero);
rpc FindMany (stream HeroById) returns (stream Hero);
}
message HeroById {
int32 id = 1;
}
message Hero {
int32 id = 1;
string name = 2;
}
@Injectable()
export class HeroService implements OnModuleInit {
@Client({
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: join(__dirname, 'hero/hero.proto'),
},
})
private client: ClientGrpc;
private heroService: HeroServiceClient;
onModuleInit() {
this.heroService = this.client.getService<HeroServiceClient>('HeroService');
}
findOne(id: number): Observable<Hero> {
return this.heroService.findOne({ id });
}
}
TCP 微服务在生产环境中需要处理连接中断后的自动重连。NestJS 底层基于 net.Socket,默认情况下 TCP 连接一旦断开,后续的 send() 调用会抛出 ECONNREFUSED 或 EPIPE 错误。应在外层实现指数退避重试,并通过 connection pool 管理多个下游微服务实例的可用性。
@Injectable()
export class CircuitBreakerProxy {
private failures = 0;
private nextTry = Date.now();
private readonly threshold = 5;
private readonly timeout = 30000;
constructor(@Inject('MATH_SERVICE') private client: ClientProxy) {}
async safeSend<T>(pattern: any, data: any): Promise<T> {
if (Date.now() < this.nextTry) {
throw new Error('Circuit breaker is open');
}
try {
const result = await lastValueFrom(
this.client.send<T>(pattern, data).pipe(timeout(this.timeout))
);
this.failures = 0;
return result;
} catch (err) {
this.failures++;
if (this.failures >= this.threshold) {
this.nextTry = Date.now() + 60000; // 熔断一分钟
}
throw err;
}
}
}
6.3 RabbitMQ 传输
RabbitMQ 通过 @MessagePattern() 实现请求/响应模式,通过 @EventPattern() 实现事件驱动模式。事件模式下,消息被发布到 exchange,不关心是否有消费者在线,天然支持服务解耦和最终一致性。
const rabbitOptions: RmqOptions = {
transport: Transport.RMQ,
options: {
urls: ['amqp://guest:guest@rabbitmq:5672'],
queue: 'orders_queue',
queueOptions: { durable: true },
noAck: false, // 手动确认
},
};
// 事件消费者(事件模式:fire and forget)
@Controller()
export class OrdersEventsController {
@EventPattern('order_created')
async handleOrderCreated(data: OrderCreatedEvent) {
await this.notificationService.sendConfirmationEmail(data.userId, data.orderId);
}
}
// 发布事件
this.clientProxy.emit('order_created', { userId, orderId, total });
对于需要可靠投递的场景,应关闭 noAck 并在业务处理完成后手动调用 channel.ack()。若处理失败,消息会重新进入队列或进入死信队列(DLQ),防止消息丢失。
@EventPattern('order_created')
async handleOrderCreated(
@Payload() data: OrderCreatedEvent,
@Ctx() context: RmqContext,
) {
const channel = context.getChannelRef();
const originalMsg = context.getMessage();
try {
await this.processOrder(data);
channel.ack(originalMsg);
} catch (err) {
channel.nack(originalMsg, false, false); // 不重新入队,进入死信队列
this.logger.error(`Order processing failed: ${err.message}`);
}
}
| 传输协议 | 模式 | 序列化 | 流支持 | 适用场景 |
|---|---|---|---|---|
| TCP | 请求/响应 | JSON | 无 | 内网同构服务通信 |
| gRPC | 请求/响应 + 双向流 | Protobuf | 原生支持 | 跨语言服务网格、高性能 RPC |
| RabbitMQ | 请求/响应 + 事件 | JSON / MessagePack | 无 | 异步事件驱动、削峰填谷 |
相关阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。