Nestjs快速入门

Nestjs快速入门

从零开始学习 NestJS,快速搭建第一个后端项目。本文将带你理解 NestJS 的核心开发模式,包括模块 Module、控制器 Controller、服务 Service、依赖注入 DI、路由接口、项目结构与常用命令,帮助前端开发者快速掌握现代 Node.js 服务端开发的基本流程。

开启SwaggerUI

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const config = new DocumentBuilder()
    .setTitle('API 文档')
    .setDescription('项目接口文档')
    .setVersion('1.0')
    .build();

  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('/api-docs', app, document);

  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

在controller中创建的dto会自动映射到swager ui:

cat.dto.ts:

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

cats.controller.ts:

@Post()
async create(@Body() createCatDto: CreateCatDto) {
  return 'This action adds a new cat';
}

添加控制器

创建控制器以及对应的test文件:

nest g controller [name]

添加模块

nest g module [name]

控制器语法

1.交由nestjs处理返回值

普通情况:

import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

想要设置请求头,但是又让nestjs接管返回值(必须加passthrough,否则nestjs不接管):

@Get()
getHello(@Res({ passthrough: true }) res: Response) {
  res.cookie('token', '12345'); // 👈 手动设置 cookie
  res.setHeader('X-Custom', 'Nest');
  return { message: 'OK' };     // 👈 Nest 仍然会自动序列化
}
接管的参数装饰器 对应express中的功能
@Request(), @Req() req
@Response(), @Res() res
@Next() next
@Session() req.session
@Param(key?: string) req.params/req.params[key]
@Body(key?: string) req.body/req.body[key]
@Query(key?: string) req.query/req.query[key]
@Headers(name?: string) req.headers/req.headers[name]
@Ip() req.ip
@HostParam() req.hosts

2.接管底层api控制返回值

import { Controller, Get, Res, Query } from '@nestjs/common';
import { Response } from 'express';
import { HttpStatus } from '@nestjs/common';

@Controller('/11')
export class AppController {
  @Get()
  getHello(@Res() res: Response, @Query('ok') ok: string) {
    if (ok === 'true') {
      return res.status(HttpStatus.OK).send('一切正常');
    } else {
      return res.status(HttpStatus.BAD_REQUEST).send('请求错误');
    }
  }
}

3.请求方法

3.1 @Get()

import { Controller, Get, Query } from '@nestjs/common';

@Controller('/user')
export class UserController {
  @Get()
  findAll(@Query('page') page: number) {
    return { message: `获取用户列表,第 ${page} 页` };
  }

  @Get(':id')
  findOne() {
    return { message: '获取单个用户信息' };
  }
}

3.2 @Post()

import { Controller, Post, Body } from '@nestjs/common';

@Controller('/user')
export class UserController {
  @Post()
  create(@Body() body: any) {
    return { message: '创建成功', data: body };
  }
}

3.3 @Put()

import { Controller, Put, Body, Param } from '@nestjs/common';

@Controller('/user')
export class UserController {
  @Put(':id')
  update(@Param('id') id: string, @Body() body: any) {
    return { message: `用户 ${id} 已更新`, data: body };
  }
}

3.4 @Delete()

import { Controller, Delete, Param } from '@nestjs/common';

@Controller('/user')
export class UserController {
  @Delete(':id')
  remove(@Param('id') id: string) {
    return { message: `用户 ${id} 已被删除` };
  }
}

3.5 @Patch()

import { Controller, Patch, Param, Body } from '@nestjs/common';

@Controller('/user')
export class UserController {
  @Patch(':id')
  partialUpdate(@Param('id') id: string, @Body() body: any) {
    return { message: `用户 ${id} 的部分字段已更新`, data: body };
  }
}

3.6 @Options()

import { Controller, Options } from '@nestjs/common';

@Controller('/user')
export class UserController {
  @Options()
  options() {
    return { allow: 'GET,POST,PUT,DELETE,PATCH,OPTIONS' };
  }
}

实际案例:

import { Controller, Options, Res } from '@nestjs/common';
import { Response } from 'express';

@Controller('/user')
export class UserController {
  @Options()
  handleOptions(@Res() res: Response) {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,PATCH,OPTIONS');
    res.header('Access-Control-Allow-Headers', 'Content-Type,Authorization');
    res.sendStatus(204); // 204 = No Content
  }
}

3.7 @Head()

import { Controller, Head, Param } from '@nestjs/common';

@Controller('/user')
export class UserController {
  @Head(':id')
  checkExists(@Param('id') id: string) {
    // 实际中只返回状态码,不返回 body
    return { message: `检测用户 ${id} 是否存在` };
  }
}

3.8 @All()

import { Controller, All, Req } from '@nestjs/common';
import { Request } from 'express';

@Controller('/any')
export class AnyController {
  @All()
  handleAll(@Req() req: Request) {
    return { method: req.method, message: '无论什么方法我都接收' };
  }
}

4.重定向

默认重定向+条件重定向:

  @Get('docs')
  @Redirect('https://baidu.com', 302)
  getDocs(@Query('version') version) {
    if (version && version === '5') {
      return { url: 'https://qq.com' };
    }
  }

5.路由参数

5.1获取参数对象

@Get(':id')
findOne(@Param() params: any): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}

5.2获取单个参数

@Get(':id')
findOne(@Param('id') id: string): string {
  return `This action returns a #${id} cat`;
}

6.子域路由

仅允许该域名访问的情况:

@Controller({ host: 'admin.example.com', path: 'user' })
export class AdminUserController {
  @Get()
  getUsers() {
    return 'admin.example.com/user';
  }
}

使用域名通配符:

@Controller({ host: ':account.example.com' })
export class AccountController {
  @Get()
  getInfo(@HostParam('account') account: string) {
    return account;
  }
}

7.异步

返回Promise:

@Get()
async findAll(): Promise<any[]> {
  return [];
}

返回RxJS可观察流:

@Get()
findAll(): Observable<any[]> {
  return of([]);
}

8.查询参数

@Get()
async findAll(@Query('age') age: number, @Query('breed') breed: string) {
  return `This action returns all cats filtered by age: ${age} and breed: ${breed}`;
}

9.在module中注册控制器

import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';

@Module({
  controllers: [CatsController],
})
export class AppModule {}

Provider语法

1.创建Service

import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}

2.在控制器中使用Service

import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  // 此处的service在ts中会通过emitDecoratorMetadata选项自动注入元数据,然后nestjs读取此数据完成注入
  constructor(private catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

3.自定义Provider注入

Provider 类型 写法 用途
Class Provider provide: A, useClass: B 用类实例作为依赖
Value Provider provide: TOKEN, useValue: xxx 用静态对象、配置、常量等作为依赖
Factory Provider provide: TOKEN, useFactory: () => something 动态生成依赖,支持注入
Existing Provider useExisting 用另一个 Provider 的实例

3.1 Value Provider

语法:

import { CatsService } from './cats.service';

const mockCatsService = {
  /* mock implementation
  ...
  */
};

@Module({
  imports: [CatsModule],
  providers: [
    {
      provide: CatsService,
      useValue: mockCatsService,
    },
  ],
})
export class AppModule {}

注:useValue会直接将对象或值注入,不会触发nest生命周期钩子,也不会在该对象或值中继续注入依赖,而是当作一个普通的对象使用

字符串方式注入:

import { connection } from './connection';

@Module({
  providers: [
    {
      provide: 'CONNECTION',
      useValue: connection,
    },
  ],
})
export class AppModule {}

@Injectable()
export class CatsRepository {
  constructor(@Inject('CONNECTION') connection: Connection) {}
}

3.2 Class Provider

// 基于不同环境变量注入
const configServiceProvider = {
  provide: ConfigService,
  useClass:
    process.env.NODE_ENV === 'development'
      ? DevelopmentConfigService
      : ProductionConfigService,
};

@Module({
  providers: [configServiceProvider],
})
export class AppModule {}

注:使用useClass方式,nestjs会自动实例化该class,且会处理该class内部的依赖注入

3.3 Factory Provider

useFactory语法允许动态创建提供程序

const connectionProvider = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider: MyOptionsProvider, optionalProvider?: string) => {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [MyOptionsProvider, { token: 'SomeOptionalProvider', optional: true }],
  //       \______________/             \__________________/
  //        必须提供                      可选,不提供时为undefined
};

@Module({
  providers: [
    connectionProvider,
    MyOptionsProvider, // class-based provider
    // { provide: 'SomeOptionalProvider', useValue: 'anything' },
  ],
})
export class AppModule {}

3.4 Existing Provider

用于提供已有provider的别名:

@Injectable()
class LoggerService {
  /* implementation details */
}

const loggerAliasProvider = {
  provide: 'AliasedLoggerService',
  useExisting: LoggerService,
};

@Module({
  providers: [LoggerService, loggerAliasProvider],
})
export class AppModule {}

3.5 Async Provider

依赖异步provider的类会在异步provider完成后才会执行

{
  provide: 'ASYNC_CONNECTION',
  useFactory: async () => {
    const connection = await createConnection(options);
    return connection;
  },
}

3.6 注入方式

基于构造函数注入

import { Injectable, Optional, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> {
  constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}

基于属性注入

import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> {
  @Inject('HTTP_OPTIONS')
  private readonly httpClient: T;
}

基于属性注入的方式是在类实例创建后在赋值的,此种方式一般只在构造函数注入方式会产生循环依赖的时候再使用

模块

过@Module的imports和exports可以导入和导出模块,@Global()可以让模块全局生效,无需再导入

import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Global()
@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})
export class CatsModule {}

动态模块

配置模块示例

创建Service:

import * as fs from 'node:fs';
import * as path from 'node:path';
import * as dotenv from 'dotenv';
import { Injectable, Inject } from '@nestjs/common';
import { EnvConfig } from './interfaces';

@Injectable()
export class ConfigService {
  private readonly envConfig: EnvConfig;

  constructor(@Inject('CONFIG_OPTIONS') private options: Record<string, any>) {
    const filePath = `${process.env.NODE_ENV || 'development'}.env`;
    const envFile = path.resolve(__dirname, '../../', options.folder, filePath);
    this.envConfig = dotenv.parse(fs.readFileSync(envFile));
  }

  get(key: string): string {
    return this.envConfig[key];
  }
}

创建模块:

import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';

@Module({})
export class ConfigModule {
    // 也常命名为forRoot/forFeature
  static register(options: Record<string, any>): DynamicModule {
    return {
      module: ConfigModule,
      providers: [
        {
          provide: 'CONFIG_OPTIONS',
          useValue: options,
        },
        ConfigService,
      ],
      exports: [ConfigService],
    };
  }
}

使用方式:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';

@Module({
  imports: [ConfigModule.register({ folder: './config' })],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

可配置模块构建器

此方法实际上是上述配置模块示例的封装

  1. 导出配置类和其Token

    import { ConfigurableModuleBuilder } from '@nestjs/common';
    import { ConfigModuleOptions } from './interfaces/config-module-options.interface';
    
    export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
      new ConfigurableModuleBuilder<ConfigModuleOptions>().build();
    
    
  2. 实现该配置

    // config.module.ts
    import { Module } from '@nestjs/common';
    import { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } from './configurable';
    
    @Module({})
    export class ConfigModule extends ConfigurableModuleClass {
      // 这里可以添加其他 providers 或 exports
    }
    
    // 使用时:
    ConfigModule.register({ host: 'localhost', port: 3000 });
    
    
  3. 在服务中获取配置

    import { Inject, Injectable } from '@nestjs/common';
    import { MODULE_OPTIONS_TOKEN } from './configurable';
    
    @Injectable()
    export class ConfigService {
      constructor(@Inject(MODULE_OPTIONS_TOKEN) private options: ConfigModuleOptions) {}
    
      getHost() {
        return this.options.host;
      }
    }
    
    

自定义该配置的方法名:

export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
  new ConfigurableModuleBuilder<ConfigModuleOptions>().setClassMethodName('xxxx').build();

@Module({
  imports: [
    ConfigModule.xxxx({ folder: './config' }), // <-- 使用自定义的方法名
    // or alternatively:
    // ConfigModule.xxxxAsync({
    //   useFactory: () => {
    //     return {
    //       folder: './config',
    //     }
    //   },
    //   inject: [...any extra dependencies...]
    // }),
  ],
})
export class AppModule {}

自定义选项工厂类:

@Module({
  imports: [
    ConfigModule.registerAsync({
      useClass: ConfigModuleOptionsFactory, // ConfigModuleOptionsFactory必须提供create()方法返回配置
    }),
  ],
})
export class AppModule {}

// setFactoryMethodName可以修改create()方法为其他名称
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
  new ConfigurableModuleBuilder<ConfigModuleOptions>().setFactoryMethodName('createConfigOptions').build();

额外参数:

在某些特殊情况下,您的模块可能需要接受额外的选项来确定其行为方式(一个很好的例子是标志isGlobal- 或只是global),但同时,这些选项不应该包含在MODULE_OPTIONS_TOKEN提供程序中(因为它们与在该模块中注册的服务/提供程序无关,例如,ConfigService不需要知道其宿主模块是否注册为全局模块)。

export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
  new ConfigurableModuleBuilder<ConfigModuleOptions>()
    .setExtras(
      {
        isGlobal: true,
      },
      (definition, extras) => ({
        ...definition,
        global: extras.isGlobal,
      }),
    )
    .build();

@Module({
  imports: [
    ConfigModule.register({
      isGlobal: true,
      folder: './config',
    }),
  ],
})
export class AppModule {}

@Injectable()
export class ConfigService {
  constructor(
    @Inject(MODULE_OPTIONS_TOKEN) private options: ConfigModuleOptions,
  ) {
    // "options" 将不会包含isGlobal属性
    // ...
  }
}

模块的循环依赖问题

服务延迟注入:

@Injectable()
export class CatsService {
  constructor(
    @Inject(forwardRef(() => CommonService))
    private commonService: CommonService,
  ) {}
}

模块延迟注入:

@Module({
  imports: [forwardRef(() => CatsModule)],
})
export class CommonModule {}

模块引用工具-ModuleRef

  1. 动态引入模块

    @Injectable()
    export class CatsService {
      constructor(private moduleRef: ModuleRef) {}
    
      async getTenantId(contextId: ContextId) {
        const userContext = await this.moduleRef.resolve(UserContextService, contextId, { strict: false });
        return userContext.tenantId;
      }
    }
    
    
  2. 动态实例化 Provider

    const myServiceInstance = await this.moduleRef.create(SomeService);
    

strict参数的作用:

moduleRef.get<T>(
  typeOrToken: Type<T> | string | symbol,
  options?: { strict: boolean }
): T
  • options.strict
    • 默认值:true
    • 控制 搜索范围
      • strict: true → 只在当前模块内部查找 provider
      • strict: false → 会在整个应用的模块树中查找 provider

get和resolve区别:

方法 获取的实例类型 是否异步 是否支持 Request-scoped 作用范围
moduleRef.get() 单例 (singleton) ❌ 不支持 当前模块(或全局)
moduleRef.resolve() 请求级 (request-scoped) ✅ 是 ✅ 支持 针对特定请求上下文

手动创建一个请求上下文

const contextId = ContextIdFactory.create();
this.moduleRef.registerRequestByContextId(/* YOUR_REQUEST_OBJECT */, contextId);

示例:

@Injectable({ scope: Scope.REQUEST })
export class RequestLoggerService {
  constructor(@Inject(REQUEST) private readonly request: Request) {}

  log() {
    console.log('来自请求:', this.request.url);
  }
}

@Injectable()
export class CatsService {
  constructor(private moduleRef: ModuleRef) {}

  async handle(req: Request) {
    // 1️⃣ 创建上下文 ID
    const contextId = ContextIdFactory.create();

    // 2️⃣ 注册请求对象和上下文 ID 的对应关系
    this.moduleRef.registerRequestByContextId(req, contextId);

    // 3️⃣ 在这个请求上下文中解析 Request-scoped provider
    const logger = await this.moduleRef.resolve(RequestLoggerService, contextId);

    // 4️⃣ 使用它
    logger.log();
  }
}

步骤 动作 说明
1️⃣ ContextIdFactory.create() 生成一个新的请求上下文 ID
2️⃣ registerRequestByContextId(req, contextId) 绑定 Express 的 req 对象到这个上下文
3️⃣ resolve(RequestScopedService, contextId) Nest 在对应上下文中创建该 service 的实例
4️⃣ 后续请求用同一 contextId Nest 复用相同实例

延迟加载模块

@Injectable()
export class CatsService {
  constructor(private lazyModuleLoader: LazyModuleLoader) {}

  async getReports() {
    // 动态加载模块  只有在真正需要的时候,才动态加载模块。
    const moduleRef = await this.lazyModuleLoader.load(() =>
      import('../reports/reports.module').then(m => m.ReportsModule),
    );

    // 从加载后的模块里拿到 provider
    const reportsService = moduleRef.get(ReportsService);

    return reportsService.generate();
  }
}

注入作用域

Scope Desciption
DEFAULT 整个应用程序共享同一个提供程序实例。实例的生命周期与应用程序的生命周期直接相关。应用程序启动后,所有单例提供程序都会被实例化。默认情况下使用单例作用域。
REQUEST **对于每个传入的请求,**都会创建一个新的提供程序实例。请求处理完成后,该实例会被垃圾回收。
TRANSIENT 瞬态提供者不会在消费者之间共享。每个注入瞬态提供者的消费者都会获得一个新的、专用的实例。

单例服务注入请求作用域的服务

类型 注入关系 可行性
单例 → 单例 可以直接注入
单例 → Request-scoped 启动时报错,需要延迟注入
Request-scoped → 单例 Request-scoped 可以依赖单例
Request-scoped → Request-scoped 每个请求生成独立实例

需要延迟注入:

@Injectable()
export class OrderService {
  constructor(private moduleRef: ModuleRef) {}

  async getOrders() {
    // 在请求上下文里获取 UserContextService
    const userContext = await this.moduleRef.resolve(UserContextService, { strict: false });
    const userId = userContext.getUserId();
    return [{ orderId: 1, userId }];
  }
}

请求作用域

@Injectable({ scope: Scope.REQUEST })
export class UserContextService {
  constructor(@Inject(REQUEST) private req: Request) {} // 在此处注入req对象
  getUserId() {
    return this.req.user.id;
  }
}

@Injectable({ scope: Scope.REQUEST })
export class OrderService {
  constructor(private userContext: UserContextService) {}
  getOrders() {
    const userId = this.userContext.getUserId(); // 自动获取
  }
}

相同租户共享同一请求作用域

import {
  HostComponentInfo,
  ContextId,
  ContextIdFactory,
  ContextIdStrategy,
} from '@nestjs/core';
import { Request } from 'express';

const tenants = new Map<string, ContextId>();

export class AggregateByTenantContextIdStrategy implements ContextIdStrategy {
  attach(contextId: ContextId, request: Request) {
    const tenantId = request.headers['x-tenant-id'] as string; // x-tenant-id可能来自jwt,然后在认证网关中携带
    let tenantSubTreeId: ContextId;

    if (tenants.has(tenantId)) {
      tenantSubTreeId = tenants.get(tenantId);
    } else {
      tenantSubTreeId = ContextIdFactory.create();
      tenants.set(tenantId, tenantSubTreeId);
    }

    // If tree is not durable, return the original "contextId" object
    return (info: HostComponentInfo) =>
      info.isTreeDurable ? tenantSubTreeId : contextId;
  }
}

补充:上述示例中,没有向request对象注册任何有效载荷,我们可以通过下述方案注入有效载荷:

注入有效载荷:

// The return of `AggregateByTenantContextIdStrategy#attach` method:
return {
  resolve: (info: HostComponentInfo) =>
    info.isTreeDurable ? tenantSubTreeId : contextId,
  payload: { tenantId },
};

使用有效载荷:

import { Injectable, Inject, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';

@Injectable({ scope: Scope.REQUEST })
export class UserContextService {
  tenantId: string;

  constructor(@Inject(REQUEST) private request: any) {
    // NestJS 会在 request 对象上挂载 contextId 的 payload
    this.tenantId = request?.contextId?.payload?.tenantId;
  }

  getTenantId() {
    return this.tenantId;
  }
}

中间件

1.创建中间件

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log('Request...');
    next();
  }
}

2.应用中间件

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('cats'); // 此处指定哪些路由使用该中间件
  }
}

2.1限制中间件生效路由的方法

import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes({ path: 'cats', method: RequestMethod.GET }); // 仅path为cats 方法为GET
  }
}

注:forRoutes也可以使用路径通配符

2.2排除路由

consumer
  .apply(LoggerMiddleware)
  .exclude(
    { path: 'cats', method: RequestMethod.GET },
    { path: 'cats', method: RequestMethod.POST },
    'cats/{*splat}',
  )
  .forRoutes(CatsController);

3.函数中间件

import { Request, Response, NextFunction } from 'express';

export function logger(req: Request, res: Response, next: NextFunction) {
  console.log(`Request...`);
  next();
};

4.全局注册中间件

const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(process.env.PORT ?? 3000);

异常过滤器

HttpException的第一个参数为响应体,可以是字符串或对象,第二个参数为状态码,第三个参数用于提供错误原因,不会序列化到响应体中

@Get()
async findAll() {
  try {
    await this.service.findAll()
  } catch (error) {
    throw new HttpException({
      status: HttpStatus.FORBIDDEN,
      error: 'This is a custom message',
    }, HttpStatus.FORBIDDEN, {
      cause: error
    });
  }
}

自定义异常

export class ForbiddenException extends HttpException {
  constructor() {
    super('Forbidden', HttpStatus.FORBIDDEN);
  }
}

内置HTTP异常(继承自HttpException)

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • HttpVersionNotSupportedException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • UnprocessableEntityException
  • InternalServerErrorException
  • NotImplementedException
  • ImATeapotException
  • MethodNotAllowedException
  • BadGatewayException
  • ServiceUnavailableException
  • GatewayTimeoutException
  • PreconditionFailedException

异常过滤器

以下代码将捕获所有HttpException以及其子类的错误

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@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,
      });
  }
}

绑定过滤器

单路径级别

@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

控制器级

@Controller()
@UseFilters(new HttpExceptionFilter())
export class CatsController {}

全局

此种方式因为自己new创建的filter实例,nestjs不会自动注入依赖,也就没法使用其他模块:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

此种方式可以自动注入依赖:

import { APP_FILTER } from '@nestjs/core';

@Module({
  providers: [
    {
      // 如果一个 provider 的 token 是 APP_FILTER → 把它当作全局过滤器注册
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
  ],
})
export class AppModule {}

捕获所有异常

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';

@Catch() // 此处不传递参数
export class CatchEverythingFilter implements ExceptionFilter {
  constructor(private readonly httpAdapterHost: HttpAdapterHost) {}

  catch(exception: unknown, host: ArgumentsHost): void {
    // In certain situations `httpAdapter` might not be available in the
    // constructor method, thus we should resolve it here.
    const { httpAdapter } = this.httpAdapterHost;

    const ctx = host.switchToHttp();

    const httpStatus =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const responseBody = {
      statusCode: httpStatus,
      timestamp: new Date().toISOString(),
      path: httpAdapter.getRequestUrl(ctx.getRequest()),
    };

    httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
  }
}

多个异常处理顺序,写在后面的先执行

以下代码,当发生BadRequest异常时,使用BadRequestFilter,而不会走AllExceptionFilter:

app.useGlobalFilters(
  new AllExceptionFilter(), // 捕获所有的先声明
  new BadRequestFilter(),   // 特定类型的后声明
);

在不改变官方异常处理器的情况下加逻辑(继承自BaseExceptionFilter):

继承方式 是否保留 Nest 默认异常处理逻辑 是否需要你手动处理所有情况 常用用途
implements ExceptionFilter(你现在的写法) ❌ 不保留,需要你自己覆盖所有逻辑 ✅ 你负责返回格式、响应内容、甚至处理 Http / 非 Http 区分 完全自定义响应格式(企业常用)
extends BaseExceptionFilter ✅ 会沿用 Nest 默认异常处理逻辑(你可以在这之上加日志/上报) ❌ 默认逻辑会自动处理大部分情况 兜底、上报、日志,而不想改变返回格式
@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    // 1. 记录日志
    console.error('🔥 全局异常:', exception);

    // 2. 上报监控系统
    // sentry.captureException(exception);

    // 3. 继续走默认异常处理
    super.catch(exception, host);
  }
}

注册方法:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const { httpAdapter } = app.get(HttpAdapterHost);
  app.useGlobalFilters(new AllExceptionsFilter(httpAdapter));

  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();


// APP_FILTER方式同前文

管道

两种典型用途:

  • 转换:将输入数据转换为所需格式(例如,从字符串转换为整数)
  • 验证:评估输入数据,如果有效,则直接传递,否则抛出异常。

内置管道

Pipe 名称 主要功能 示例输入 转换/验证结果 失败时行为
ValidationPipe 基于 DTO 和 class-validator 自动验证请求体 { name: 123 } ❌ 不符合 DTO 则不通过 抛出 400 Bad Request
ParseIntPipe 将字符串转换为整数 "12" 12 (number) 抛异常:不是可转整型的值
ParseFloatPipe 将字符串转换为浮点数 "3.14" 3.14 (number) 抛异常:不是可转浮点的值
ParseBoolPipe 将值转换为布尔 "true" / "1" true 抛异常:值无法解释为布尔
ParseArrayPipe 转换为数组,可指定每项类型 "a,b,c"["a","b"] ["a","b","c"] 抛异常:格式不正确或元素类型不匹配
ParseUUIDPipe 校验是否是 UUID(默认 v4) "0f8fad5b-d9cb-469f-a165-70867728950e" ✅ 原样通过 抛异常:不是合法 UUID
ParseEnumPipe 校验参数是否属于 Enum "admin" 返回对应枚举值 抛异常:值不在枚举中
DefaultValuePipe 参数未提供时设定默认值 undefined 替换为默认值,如 1 不抛错
ParseFilePipe 处理 & 验证上传文件(大小/类型等) 上传文件 返回文件对象 抛异常:文件不符合要求
ParseDatePipe 将字符串转换为 Date "2025-06-01" new Date("2025-06-01") 抛异常:无法转换为时间类型

连接管道

@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) { // id不为整数就会报错
  return this.catsService.findOne(id);
}

实例化的方式传入:

@Get(':id')
async findOne(
  @Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
  id: number,
) {
  return this.catsService.findOne(id);
}

自定义管道

import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    return value;
  }
}

transform 方法参数解释:

参数 含义 示例
value 路由中传入的参数值 比如 @Param('id') 里的值
metadata 当前参数的元数据信息 包含参数是在 Body、Query 还是 Param

metadata 的结构:

{
  type: 'body' | 'query' | 'param' | 'custom';
  metatype?: Function;  // 参数的 TypeScript 类型(如果有)
  data?: string;        // 参数的键名
}

示例:

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';

@Injectable()
export class ValidationPipe implements PipeTransform {
  async transform(value: any, metadata: ArgumentMetadata) {
    const { metatype } = metadata;

    // 如果没有类型或是原始类型,不验证
    if (!metatype || this.isPrimitive(metatype)) {
      return value;
    }

    // 转换成 DTO 类实例
    const object = plainToInstance(metatype, value);
    const errors = await validate(object);

    if (errors.length > 0) {
      throw new BadRequestException('Validation failed');
    }

    return value;
  }

  private isPrimitive(meta): boolean {
    return [String, Boolean, Number, Array, Object].includes(meta);
  }
}

对象模式验证

使用三方库zod

  1. 创建管道
import { PipeTransform, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ZodSchema  } from 'zod';

export class ZodValidationPipe implements PipeTransform {
  constructor(private schema: ZodSchema) {}

  transform(value: unknown, metadata: ArgumentMetadata) {
    try {
      const parsedValue = this.schema.parse(value);
      return parsedValue;
    } catch (error) {
      throw new BadRequestException('Validation failed');
    }
  }
}
  1. 创建对象验证模式
import { z } from 'zod';

export const createCatSchema = z
  .object({
    name: z.string(),
    age: z.number(),
    breed: z.string(),
  })
  .required();

export type CreateCatDto = z.infer<typeof createCatSchema>;

  1. 绑定对象验证
@Post()
@UsePipes(new ZodValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

使用class-validator

依赖安装:

$ npm i --save class-validator class-transformer

创建dto:

import { IsString, IsInt } from 'class-validator';

export class CreateCatDto {
  @IsString()
  name: string;

  @IsInt()
  age: number;

  @IsString()
  breed: string;
}

创建验证管道:

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
  async transform(value: any, { metatype }: ArgumentMetadata) {
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    const object = plainToInstance(metatype, value);
    const errors = await validate(object);
    if (errors.length > 0) {
      throw new BadRequestException('Validation failed');
    }
    return value;
  }

  private toValidate(metatype: Function): boolean {
    const types: Function[] = [String, Boolean, Number, Array, Object];
    return !types.includes(metatype);
  }
}

绑定管道:

  1. 参数级:
@Post()
async create(
  @Body(new ValidationPipe()) createCatDto: CreateCatDto,
) {
  this.catsService.create(createCatDto);
}
  1. 全局级
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

支持依赖注入的全局级绑定:

import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_PIPE,
      useClass: ValidationPipe,
    },
  ],
})
export class AppModule {}
  

绑定在不同作用域的管道:

作用域 示例 生效范围
参数级 @Body(new ValidationPipe()) dto: Dto 只对该参数生效
方法级 @UsePipes(new ValidationPipe()) 放在方法上 对该方法的所有参数生效
控制器级 @UsePipes(new ValidationPipe()) 放在控制器上 对该控制器中所有路由生效
全局级 app.useGlobalPipes(new ValidationPipe()) 对整个应用的所有请求生效

排除属性

import { Exclude } from 'class-transformer';

export class UserEntity {
  id: number;
  firstName: string;
  lastName: string;

  @Exclude()
  password: string;

  constructor(partial: Partial<UserEntity>) {
    Object.assign(this, partial);
  }
}

@UseInterceptors(ClassSerializerInterceptor)
@Get()
findOne(): UserEntity {
  return new UserEntity({
    id: 1,
    firstName: 'John',
    lastName: 'Doe',
    password: 'password', // 此字段不会返回给用户
  });
}

属性别名或组合属性

@Expose()
get fullName(): string {
  return `${this.firstName} ${this.lastName}`;
}

转换属性

@Transform(({ value }) => value.name)
role: RoleEntity;

序列化时,自动排除所有以 _ 开头的属性

@SerializeOptions({
  excludePrefixes: ['_'],
})
@Get()
findOne(): UserEntity {
  return new UserEntity();
}

使用序列化拦截器将普通对象转化为对象

@UseInterceptors(ClassSerializerInterceptor)
@SerializeOptions({ type: UserEntity })
@Get()
findOne(@Query() { id }: { id: number }): UserEntity {
  if (id === 1) {
    return {
      id: 1,
      firstName: 'John',
      lastName: 'Doe',
      password: 'password',
    };
  }

  return {
    id: 2,
    firstName: 'Kamil',
    lastName: 'Mysliwiec',
    password: 'password2',
  };
}

守卫

绑定gurards:

// 控制器级别
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}


@Controller('cats')
@UseGuards(new RolesGuard())
export class CatsController {}


// 全局级别

const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());



import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ],
})
export class AppModule {}


利用自定义元数据实现不同controller拥有不同的权限:

  1. 创建自定义装饰器(roles.decorator.ts)
import { Reflector } from '@nestjs/core';

export const Roles = Reflector.createDecorator<string[]>();

  1. 使用自定义装饰器标记
@Post()
@Roles(['admin'])
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
  1. 实现守卫
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from './roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get(Roles, context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return matchRoles(roles, user.roles);
  }
}

类/方法元数据读取对比:

读取对象 使用场景 举例 返回值
context.getHandler() 方法级别权限控制 @Roles(['admin']) 放在方法上 ['admin']
context.getClass() 类级别默认权限 @Roles(['superadmin']) 放在类上 ['superadmin']
组合 方法优先,类为默认 方法没贴,类贴了 ['superadmin']

优先获取方法元数据,没有再回退类元数据的方案:

const roles = this.reflector.getAllAndOverride(Roles, [context.getHandler(), context.getClass()]);

// 等价于以下代码
const roles = 
  this.reflector.get(Roles, context.getHandler()) ||
  this.reflector.get(Roles, context.getClass());

拦截器

主要用途:

  • 方法执行前后绑定额外的逻辑
  • 转换函数返回的结果
  • 转换函数抛出的异常
  • 扩展基本功能行为
  • 根据特定条件完全重写函数(例如,出于缓存目的)

拦截路由打印日志:

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Before...');

    const now = Date.now();
    return next
      .handle()
      .pipe(
        tap(() => console.log(`After... ${Date.now() - now}ms`)),
      );
  }
}

绑定拦截器:

// 控制器级
@UseInterceptors(LoggingInterceptor)
export class CatsController {}



@UseInterceptors(new LoggingInterceptor())
export class CatsController {}


// 全局级
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());



import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: LoggingInterceptor,
    },
  ],
})
export class AppModule {}

转换返回数据示例:

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface Response<T> {
  data: T;
}

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
    return next.handle().pipe(map(data => ({ data })));
  }
}

自定义装饰器

参数装饰器

创建一个在req上获取user的装饰器:

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);


使用该装饰器:

@Get()
async findOne(@User() user: UserEntity) {
  console.log(user);
}

配合管道工作:

必须加validateCustomDecorators,否则由自定义装饰器转换后返回的数据不会被验证!

@Get()
async findOne(
  @User(new ValidationPipe({ validateCustomDecorators: true }))
  user: UserEntity,
) {
  console.log(user);
}

创建装饰器组合

import { applyDecorators } from '@nestjs/common';

export function Auth(...roles: Role[]) {
  return applyDecorators(
    SetMetadata('roles', roles),
    UseGuards(AuthGuard, RolesGuard),
    ApiBearerAuth(),
    ApiUnauthorizedResponse({ description: 'Unauthorized' }),
  );
}

使用组合装饰器:

@Get('users')
@Auth('admin')
findAllUsers() {}

两种自定义装饰器的创建方法:

// 方法一
import { Reflector } from '@nestjs/core';
export const Roles = Reflector.createDecorator<string[]>();

// 方法一的读取方法
const roles = this.reflector.get(Roles, context.getHandler());

// 方法二
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

// 方法二的读取方法
const roles = this.reflector.get<string[]>('roles', context.getHandler());

执行上下文

对比项 ArgumentsHost ExecutionContext
来源 通用上下文对象 ArgumentsHost 的子类
场景 异常过滤器、全局拦截、底层适配器 守卫、拦截器、管道
功能 访问请求参数(req、res 等) 访问参数 + 控制器类 + 方法信息
方法 switchToHttp() / switchToWs() / switchToRpc() 同上 + getHandler()getClass()
典型应用 ExceptionFilter Guard / Interceptor / Pipe

生命周期事件

img

生命周期钩法 生命周期事件触发钩子方法调用
onModuleInit() 当宿主模块的依赖项解析完毕后调用。
onApplicationBootstrap() 在所有模块初始化完成后,但在监听连接之前调用。
onModuleDestroy()* SIGTERM在收到终止信号(例如,)后调用。
beforeApplicationShutdown()* 在所有onModuleDestroy()处理程序完成(Promise 已解决或已拒绝)后调用; 一旦完成(Promise 已解决或已拒绝),所有现有连接都将被关闭(app.close()调用)。
onApplicationShutdown()* 连接关闭后调用(app.close()解析)。

配置文件处理(@nestjs/config)

// src/config/config.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import configuration from './configuration';
import { validationSchema } from './validation';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true, // 全局可用,不需要重复 import
      load: [configuration], // 加载上面的配置函数
      validationSchema, // 校验环境变量, 使用Joi
      envFilePath: `.env`, // 可自定义 .env 路径
    }),
  ],
})
export class AppConfigModule {}

接口版本控制

启用版本控制

app.enableVersioning({
  type: VersioningType.URI,
});

使用不同版本的api

import { Controller, Get, Version } from '@nestjs/common';

@Controller('users')
export class UsersController {

  @Get()
  @Version('1')
  getV1() {
    return 'This is v1';
  }

  @Get()
  @Version('2')
  getV2() {
    return 'This is v2';
  }
}

整个controller都是v1:

@Controller({
  path: 'products',
  version: '1',
})
export class ProductsController {
  @Get()
  findAll() {
    return 'v1 products';
  }
}

支持多个版本:

@Version(['1', '2'])
@Get()
getMultiVersion() {
  return 'Support v1 & v2';
}

使用Header的版本控制

const app = await NestFactory.create(AppModule);
app.enableVersioning({
  type: VersioningType.HEADER,
  header: 'Custom-Header',
});
await app.listen(process.env.PORT ?? 3000);

使用Accept: application/json;v=2做版本控制

const app = await NestFactory.create(AppModule);
app.enableVersioning({
  type: VersioningType.MEDIA_TYPE,
  key: 'v=',
});
await app.listen(process.env.PORT ?? 3000);

版本无关路由

import { Controller, Get, VERSION_NEUTRAL } from '@nestjs/common';

@Controller({
  version: VERSION_NEUTRAL,
})
export class CatsController {
  @Get('cats')
  findAll(): string {
    return 'This action returns all cats regardless of version';
  }
}

默认全局版本

app.enableVersioning({
  // ...
  defaultVersion: '1'
  // or
  defaultVersion: ['1', '2']
  // or
  defaultVersion: VERSION_NEUTRAL
});

任务调度

npm install --save @nestjs/schedule

在需要引入任务调度的模块导入

import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';

@Module({
  imports: [
    ScheduleModule.forRoot()
  ],
})
export class AppModule {}

在Service中调度任务

import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';

@Injectable()
export class TasksService {
  private readonly logger = new Logger(TasksService.name);

  @Cron('45 * * * * *')
  handleCron() {
    this.logger.debug('Called when the current second is 45');
  }
}

@Cron支持的参数

第一个参数(string):

* * * * * *	每秒钟
45 * * * * *	每分钟,在第 45 秒
0 10 * * * *	每小时的第 10 分钟开始时
0 */30 9-17 * * *	上午9点至下午5点,每30分钟一班
0 30 11 * * 1-5	周一至周五上午11:30
CronExpression.EVERY_30_SECONDS // CronExpression提供常用时间

第二个参数(object):

name	用于在声明 cron 作业后访问和控制该作业。
timeZone	指定执行时区。这将根据您所在的时区调整实际时间。如果时区无效,则会抛出错误。您可以在Moment Timezone网站上查看所有可用的时区。
utcOffset	这样,您就可以指定时区的偏移量,而无需使用timeZone参数。
waitForCompletion	如果设置true了此参数,则在当前 onTick 回调完成之前,不会运行该 cron 作业的任何其他实例。在当前 cron 作业运行时发生的任何新的计划执行都将被完全跳过。
disabled	这表明该任务是否会被执行。

间隔执行

@Interval(10000)
@Interval('notifications', 2500)


// 第二中方式举例(写了名字可以取消它):
import { SchedulerRegistry, Injectable } from '@nestjs/common';
import { Interval } from '@nestjs/schedule';

@Injectable()
export class TasksService {
  constructor(private schedulerRegistry: SchedulerRegistry) {}

  @Interval('notifications', 2500)
  handleInterval() {
    console.log('Interval task running');
  }

  stopTask() {
    const interval = this.schedulerRegistry.getInterval('notifications');
    clearInterval(interval);
    this.schedulerRegistry.deleteInterval('notifications');
  }
}

服务启动后延迟执行

@Timeout('notifications', 2500)
handleTimeout() {}

队列

npm install --save @nestjs/bullmq bullmq

配置队列

import { Module } from '@nestjs/common';
import { BullModule } from '@nestjs/bullmq';

@Module({
  imports: [
    BullModule.forRoot({
      connection: {
        host: 'localhost',
        port: 6379,
      },
    }),
  ],
})
export class AppModule {}

  • connection: ConnectionOptions- 用于配置 Redis 连接的选项。有关更多信息,请参阅“连接”部分。可选。
  • prefix: string- 所有队列键的前缀。可选。
  • defaultJobOptions: JobOpts- 用于控制新作业默认设置的选项。有关更多信息,请参阅“作业选项”。(可选)
  • settings: AdvancedSettings- 高级队列配置设置。这些设置通常不应更改。有关更多信息,请参阅“高级设置”。可选。
  • extraOptions- 模块初始化的其他选项。请参阅手动注册。

命名配置

BullModule.forRoot('alternative-config', {
  connection: {
    port: 6381,
  },
});

注册队列

BullModule.registerQueue({
  name: 'audio',
  // 可选 会覆盖默认的配置
  connection: {
    port: 6380,
  },
});

注册命名配置

BullModule.registerQueue({
  configKey: 'alternative-config',
  name: 'video',
});

生产者

import { Injectable } from '@nestjs/common';
import { Queue } from 'bullmq';
import { InjectQueue } from '@nestjs/bullmq';

@Injectable()
export class AudioService {
  constructor(@InjectQueue('audio') private audioQueue: Queue) {
	const job = await this.audioQueue.add('transcode', {
  		foo: 'bar',
	},{
        // 额外的options
    });
  }
}

支持的options:

  • prioritynumber- 可选的优先级值。取值范围从 1(最高优先级)到 MAX_INT(最低优先级)。请注意,使用优先级会对性能产生轻微影响,因此请谨慎使用。
  • delay-number等待此任务处理的时间(毫秒)。请注意,为了确保延迟时间的准确性,服务器和客户端的时钟必须同步。
  • attemptsnumber- 尝试执行该任务直至完成的总次数。
  • repeatRepeatOpts- 根据 cron 规范重复执行任务。请参阅RepeatOpts
  • backoffnumber | BackoffOpts- 退避设置,用于在作业失败时自动重试。请参阅BackoffOpts
  • lifoboolean- 如果为真,则将作业添加到队列的右端而不是左端(默认为假)。
  • jobIdnumber| string- 覆盖作业 ID - 默认情况下,作业 ID 是一个唯一的整数,但您可以使用此设置覆盖它。如果您使用此选项,则必须确保作业 ID 是唯一的。如果您尝试添加一个 ID 已存在的作业,则不会添加该作业。
  • removeOnComplete-boolean | number如果为真,则在作业成功完成后将其移除。一个数字指定要保留的作业数量。默认行为是将作业保留在已完成作业集中。
  • removeOnFail-boolean | number如果为真,则在所有尝试失败后删除该作业。一个数字指定要保留的作业数量。默认行为是将作业保留在失败集中。
  • stackTraceLimitnumber- 限制堆栈跟踪中记录的堆栈跟踪行数。

消费者

import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Job } from 'bullmq';

@Processor('audio')
export class AudioConsumer extends WorkerHost {
  async process(job: Job<any, any, string>): Promise<any> {
    switch (job.name) {
      case 'transcode': {
        let progress = 0;
        for (i = 0; i < 100; i++) {
          await doSomething(job.data);
          progress += 1;
          await job.progress(progress);
        }
        return {};
      }
      case 'concatenate': {
        await doSomeLogic2();
        break;
      }
    }
  }
}

请求范围的消费者

@Processor({
  name: 'audio',
  scope: Scope.REQUEST,
})
export class xxx extends WorkerHost{
	constructor(@Inject(JOB_REF) jobRef: Job) {
  		console.log(jobRef);
	}
}

事件监听队列变化

在消费者类中监听

import { Processor, Process, OnWorkerEvent } from '@nestjs/bullmq';
import { Job } from 'bullmq';

@Processor('audio')
export class AudioConsumer {
  @OnWorkerEvent('active')
  onActive(job: Job) {
    console.log(
      `Processing job ${job.id} of type ${job.name} with data ${job.data}...`,
    );
  }

  // ...
}

在专门的事件中监听

import {
  QueueEventsHost,
  QueueEventsListener,
  OnQueueEvent,
} from '@nestjs/bullmq';

@QueueEventsListener('audio')
export class AudioEventsListener extends QueueEventsHost {
  @OnQueueEvent('active')
  onActive(job: { jobId: string; prev?: string }) {
    console.log(`Processing job ${job.jobId}...`);
  }

  // ...
}

队列管理

await audioQueue.pause(); // 暂停消费队列(任务会堆积)
await audioQueue.resume(); // 恢复队列消费

内置日志记录器

简单传参

// 禁用
const app = await NestFactory.create(AppModule, {
  logger: false,
});
await app.listen(process.env.PORT ?? 3000);


// 输出等级配置
const app = await NestFactory.create(AppModule, {
  logger: ['error', 'warn'],
});
await app.listen(process.env.PORT ?? 3000);

复杂选项

const app = await NestFactory.create(AppModule, {
  logger: new ConsoleLogger({
    // options
  }),
});

选项 描述 默认
logLevels 已启用日志级别。 ['log', 'fatal', 'error', 'warn', 'debug', 'verbose']
timestamp json启用后,将打印当前日志消息与上一条日志消息之间的时间戳(时间差)。注意:启用此选项后,其他功能将不再使用。 false
prefix 每条日志消息要使用的前缀。注意:json启用此选项后,此选项将不起作用。 Nest
json 启用后,将以 JSON 格式打印日志消息。 false
colors 启用后,将以彩色打印日志消息。如果禁用 JSON,则默认为 true;否则为 false。 true
context 日志记录器的上下文。 undefined
compact 启用此选项后,即使对象包含多个属性,日志消息也会打印在一行中。如果设置为一个数字,则只要所有属性的长度不超过 breakLength,最内层的 n 个元素就会合并到一行中。较短的数组元素也会被合并在一起。 true
maxArrayLength 指定格式化时要包含的 Array、TypedArray、Map、Set、WeakMap 和 WeakSet 元素的最大数量。设置为 null 或 Infinity 表示显示所有元素。设置为 0 或负数表示不显示任何元素。json启用此设置、禁用颜色以及compact设置为 true 时,此设置将被忽略,因为它会生成可解析的 JSON 输出。 100
maxStringLength 指定格式化时要包含的最大字符数。设置为 null 或 Infinity 表示显示所有元素。设置为 0 或负数表示不显示任何字符。json启用此设置、禁用颜色且compact设置为 true 时,此设置将被忽略,因为它会生成可解析的 JSON 输出。 10000
sorted 启用后,将在格式化对象时对键进行排序。也可以使用自定义排序函数。启用此选项后json,颜色将被禁用,并且compact由于会生成可解析的 JSON 输出,因此此选项将被忽略。 false
depth 指定格式化对象时递归的次数。这对于检查大型对象非常有用。要递归到最大调用堆栈大小,请传递 Infinity 或 null。json启用此功能后,颜色将被禁用,并且compact设置为 true 时,此参数将被忽略,因为它会生成可解析的 JSON 输出。 5
showHidden 如果为真,则对象的不可枚举符号和属性将包含在格式化结果中。WeakMap 和 WeakSet 条目以及用户定义的原型属性也将包含在内。 false
breakLength 输入值在多行之间的分割长度。设置为 Infinity 可将输入格式化为单行(需同时将“compact”设置为 true)。当“compact”为 true 时,默认值为 Infinity;否则为 80。json启用“compact”、禁用颜色且compact设置为 true 时,此参数将被忽略,因为它会生成可解析的 JSON 输出。 Infinity

记录日志

import { Logger, Injectable } from '@nestjs/common';

@Injectable()
class MyService {
  private readonly logger = new Logger(MyService.name);

  doSomething() {
    this.logger.log('Doing something...');
  }
}

自定义日志记录器

import { LoggerService, Injectable } from '@nestjs/common';

@Injectable()
export class MyLogger implements LoggerService {
  /**
   * Write a 'log' level log.
   */
  log(message: any, ...optionalParams: any[]) {}

  /**
   * Write a 'fatal' level log.
   */
  fatal(message: any, ...optionalParams: any[]) {}

  /**
   * Write an 'error' level log.
   */
  error(message: any, ...optionalParams: any[]) {}

  /**
   * Write a 'warn' level log.
   */
  warn(message: any, ...optionalParams: any[]) {}

  /**
   * Write a 'debug' level log.
   */
  debug?(message: any, ...optionalParams: any[]) {}

  /**
   * Write a 'verbose' level log.
   */
  verbose?(message: any, ...optionalParams: any[]) {}
}

const app = await NestFactory.create(AppModule, {
  logger: new MyLogger(),
});
await app.listen(process.env.PORT ?? 3000);

Cookie

安装

$ npm i cookie-parser
$ npm i -D @types/cookie-parser
import * as cookieParser from 'cookie-parser';
// somewhere in your initialization file
app.use(cookieParser());

读取/设置cookie

@Get()
findAll(@Req() request: Request) {
  console.log(request.cookies); // or "request.cookies['cookieKey']"
  // or console.log(request.signedCookies);
}
@Get()
findAll(@Res({ passthrough: true }) response: Response) {
  response.cookie('key', 'value')
}

利用自定义参数装饰器

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const Cookies = createParamDecorator((data: string, ctx: ExecutionContext) => {
  const request = ctx.switchToHttp().getRequest();
  return data ? request.cookies?.[data] : request.cookies;
});
@Get()
findAll(@Cookies('name') name: string) {}

响应压缩

$ npm i --save compression
$ npm i --save-dev @types/compression
import * as compression from 'compression';
// somewhere in your initialization file
app.use(compression());

注:对于流量大的网站,将这个压缩放在nginx等反向代理中性能会更好一些

文件上传

$ npm i -D @types/multer

简单单个文件

@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file: Express.Multer.File) {
  console.log(file);
}

验证文件

import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

@Injectable()
export class FileSizeValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    // "value" is an object containing the file's attributes and metadata
    const oneKb = 1000;
    return value.size < oneKb;
  }
}
@Post('file')
@UseInterceptors(FileInterceptor('file'))
uploadFileAndValidate(@UploadedFile(
  new FileSizeValidationPipe(),
  // other pipes can be added here
) file: Express.Multer.File, ) {
  return file;
}

流媒体文件

import { Controller, Get, StreamableFile, Res } from '@nestjs/common';
import { createReadStream } from 'node:fs';
import { join } from 'node:path';
import type { Response } from 'express'; // Assuming that we are using the ExpressJS HTTP Adapter

@Controller('file')
export class FileController {
  @Get()
  getFile(): StreamableFile {
    const file = createReadStream(join(process.cwd(), 'package.json'));
    return new StreamableFile(file, {
      type: 'application/json',
      disposition: 'attachment; filename="package.json"',
      // If you want to define the Content-Length value to another value instead of file's length:
      // length: 123,
    });
  }

  // Or even:
  @Get()
  getFileChangingResponseObjDirectly(@Res({ passthrough: true }) res: Response): StreamableFile {
    const file = createReadStream(join(process.cwd(), 'package.json'));
    res.set({
      'Content-Type': 'application/json',
      'Content-Disposition': 'attachment; filename="package.json"',
    });
    return new StreamableFile(file);
  }

  // Or even:
  @Get()
  @Header('Content-Type', 'application/json')
  @Header('Content-Disposition', 'attachment; filename="package.json"')
  getFileUsingStaticValues(): StreamableFile {
    const file = createReadStream(join(process.cwd(), 'package.json'));
    return new StreamableFile(file);
  }  
}

axios

安装

$ npm i --save @nestjs/axios axios

导入

@Module({
  imports: [HttpModule],
  providers: [CatsService],
})
export class CatsModule {}

使用

@Injectable()
export class CatsService {
  constructor(private readonly httpService: HttpService) {}

  findAll(): Observable<AxiosResponse<Cat[]>> {
    return this.httpService.get('http://localhost:3000/cats');
  }
}

Session

import * as session from 'express-session';
import * as connectRedis from 'connect-redis';
import Redis from 'ioredis';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const RedisStore = connectRedis(session);
  const redisClient = new Redis();

  app.use(
    session({
      store: new RedisStore({ client: redisClient }),
      secret: 'your-secret-key',
      resave: false,
      saveUninitialized: false,
      cookie: { maxAge: 3600 * 1000 }, // 1小时
    }),
  );

  await app.listen(3000);
}
bootstrap();

SSE(服务器推送事件)

利用http长连接持续获取服务器推送的事件

@Sse('sse')
sse(): Observable<MessageEvent> {
  return interval(1000).pipe(map((_) => ({ data: { hello: 'world' } }))); // interval来自RxJS
}

MessageEvent定义:

export interface MessageEvent {
  data: string | object;
  id?: string;
  type?: string;
  retry?: number;
}

前端获取event的方法

const eventSource = new EventSource('/sse');
eventSource.onmessage = ({ data }) => {
  console.log('New message', JSON.parse(data));
};

JWT验证

$ npm install --save @nestjs/jwt

导入jwt模块

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { jwtConstants } from './constants';

@Module({
  imports: [
    UsersModule,
    JwtModule.register({
      global: true, // 注册为全局模块
      secret: jwtConstants.secret,
      signOptions: { expiresIn: '60s' },
    }),
  ],
  providers: [AuthService],
  controllers: [AuthController],
  exports: [AuthService],
})
export class AuthModule {}

创建用于签名的service

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService
  ) {}

  async signIn(
    username: string,
    pass: string,
  ): Promise<{ access_token: string }> {
    const user = await this.usersService.findOne(username);
    if (user?.password !== pass) {
      throw new UnauthorizedException();
    }
    const payload = { sub: user.userId, username: user.username };
    return {
      access_token: await this.jwtService.signAsync(payload),
    };
  }
}

创建路由守卫

import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { Request } from 'express';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);
    if (!token) {
      throw new UnauthorizedException();
    }
    try {
      const payload = await this.jwtService.verifyAsync(
        token,
        {
          secret: jwtConstants.secret
        }
      );
      // 💡 We're assigning the payload to the request object here
      // so that we can access it in our route handlers
      request['user'] = payload;
    } catch {
      throw new UnauthorizedException();
    }
    return true;
  }

  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

在控制器中实现登录及获取用户信息

import {
  Body,
  Controller,
  Get,
  HttpCode,
  HttpStatus,
  Post,
  Request,
  UseGuards
} from '@nestjs/common';
import { AuthGuard } from './auth.guard';
import { AuthService } from './auth.service';

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}

  @HttpCode(HttpStatus.OK)
  @Post('login')
  signIn(@Body() signInDto: Record<string, any>) {
    return this.authService.signIn(signInDto.username, signInDto.password);
  }

  @UseGuards(AuthGuard)
  @Get('profile')
  getProfile(@Request() req) {
    return req.user;
  }
}

注册全局守卫(可选)

用途:默认所有路由都需要验证,只有声明可以放开的路由才不认证

providers: [
  {
    provide: APP_GUARD,
    useClass: AuthGuard,
  },
],

创建自定义元数据

import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

修改守卫

读取元数据判断是否为公开的路由

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private jwtService: JwtService, private reflector: Reflector) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) {
      // 💡 See this condition
      return true;
    }

    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);
    if (!token) {
      throw new UnauthorizedException();
    }
    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secret: jwtConstants.secret,
      });
      // 💡 We're assigning the payload to the request object here
      // so that we can access it in our route handlers
      request['user'] = payload;
    } catch {
      throw new UnauthorizedException();
    }
    return true;
  }

  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

指定公开的路由

@Public()
@Get()
findAll() {
  return [];
}

授权

创建角色守卫

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles) {
      return true;
    }
    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}

全局注册

providers: [
  {
    provide: APP_GUARD,
    useClass: RolesGuard,
  },
],

创建自定义装饰器

import { SetMetadata } from '@nestjs/common';
import { Role } from '../enums/role.enum';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);

让指定路由支持授权

@Post()
@Roles(Role.Admin)
create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

CASL

特性 路由守卫 CASL
权限粒度 粗(API 级别) 细(业务资源级别)
判断依据 角色、jwt、有无权限标识 具体资源字段(如 authorId)
典型应用 控制接口访问 控制数据操作、前端组件权限
能否处理条件权限 ❌很难做 ✔️天然支持
能否前后端统一使用 ❌不能 ✔️可前端复用权限规则

todo

Helmet

在headers中自动添加安全参数

安全风险 Helmet 的保护方式
XSS 攻击 Content-Security-Policy(CSP)
点击劫持 X-Frame-Options
MIME 类型伪造 X-Content-Type-Options
浏览器信息泄露 关闭 X-Powered-By
DNS 缓存投毒 DNS Prefetch 控制
HTTPS 加固 Strict-Transport-Security
$ npm i --save helmet
import helmet from 'helmet';
// somewhere in your initialization file
app.use(helmet());

CORS配置

const app = await NestFactory.create(AppModule);

app.enableCors({
  origin: [
    'http://localhost:3000',
    'https://my-site.com',
  ],
  methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
  allowedHeaders: ['Content-Type', 'Authorization'],
  exposedHeaders: ['Content-Length', 'X-My-Custom-Header'],
  credentials: true,
  maxAge: 3600,
  preflightContinue: false,
  optionsSuccessStatus: 204,
});

await app.listen(3000);

CSRF(XSRF)防护

原理:登录完成后,会设置一个cookie,调用后端的csrf接口后再次设置cookie中的csrf-token字段,然后该接口也会返回csrf-token(关键:跨域请求是拿不到响应值中的token的), 然后后续的每次请求都需要在请求头中携带该token(x-csrf-token)

import { doubleCsrf } from 'csrf-csrf';
// ...
// somewhere in your initialization file
const {
  invalidCsrfTokenError, // This is provided purely for convenience if you plan on creating your own middleware.
  generateToken, // Use this in your routes to generate and provide a CSRF hash, along with a token cookie and token.
  validateRequest, // Also a convenience if you plan on making your own middleware.
  doubleCsrfProtection, // This is the default CSRF protection middleware.
} = doubleCsrf(doubleCsrfOptions);
app.use(doubleCsrfProtection);

实现前端获取token接口

// NestJS Controller 示例
@Controller('csrf')
export class CsrfController {
  constructor(private readonly csrfFactory: CaslCsrfFactory) {}

  @Get('token')
  getCsrfToken(@Res() res: Response) {
    const token = generateToken(res); // generateToken 会设置 cookie 并返回 token
    return res.json({ csrfToken: token });
  }
}

限制请求速度(QPS)

$ npm i --save @nestjs/throttler

Websockets

$ npm i --save @nestjs/websockets @nestjs/platform-socket.io
import {
  WebSocketGateway,
  SubscribeMessage,
  MessageBody,
  ConnectedSocket,
  WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';

@WebSocketGateway({
  cors: {
    origin: '*',
  },
  namespace: 'chat'
})
export class ChatGateway {
  @WebSocketServer()
  server: Server;

  // 监听客户端连接
  handleConnection(client: Socket) {
    const token = client.handshake.headers.authorization;
    if (!token) {
      	client.disconnect();
    }
  }

  // 监听客户端断开
  handleDisconnect(client: Socket) {
    console.log(`Client disconnected: ${client.id}`);
  }

  // 监听客户端事件
  @SubscribeMessage('message')
  handleMessage(
    @ConnectedSocket() client: Socket,
    @MessageBody() message: string,
  ) {
    console.log(`Received: ${message}`);
    // 回发给所有客户端
    this.server.emit('message', `Server received: ${message}`);
  }
}

多个gateway拆分

@WebSocketGateway({ namespace: 'chat' })
export class ChatGateway {}

@WebSocketGateway({ namespace: 'notice' })
export class NoticeGateway {}