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 {}
可配置模块构建器
此方法实际上是上述配置模块示例的封装
-
导出配置类和其Token
import { ConfigurableModuleBuilder } from '@nestjs/common'; import { ConfigModuleOptions } from './interfaces/config-module-options.interface'; export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder<ConfigModuleOptions>().build(); -
实现该配置
// 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 }); -
在服务中获取配置
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
-
动态引入模块
@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; } } -
动态实例化 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→ 只在当前模块内部查找 providerstrict: 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)
BadRequestExceptionUnauthorizedExceptionNotFoundExceptionForbiddenExceptionNotAcceptableExceptionRequestTimeoutExceptionConflictExceptionGoneExceptionHttpVersionNotSupportedExceptionPayloadTooLargeExceptionUnsupportedMediaTypeExceptionUnprocessableEntityExceptionInternalServerErrorExceptionNotImplementedExceptionImATeapotExceptionMethodNotAllowedExceptionBadGatewayExceptionServiceUnavailableExceptionGatewayTimeoutExceptionPreconditionFailedException
异常过滤器
以下代码将捕获所有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
- 创建管道
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');
}
}
}
- 创建对象验证模式
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>;
- 绑定对象验证
@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);
}
}
绑定管道:
- 参数级:
@Post()
async create(
@Body(new ValidationPipe()) createCatDto: CreateCatDto,
) {
this.catsService.create(createCatDto);
}
- 全局级
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拥有不同的权限:
- 创建自定义装饰器(roles.decorator.ts)
import { Reflector } from '@nestjs/core';
export const Roles = Reflector.createDecorator<string[]>();
- 使用自定义装饰器标记
@Post()
@Roles(['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
- 实现守卫
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 |
生命周期事件

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