NestJs
基础概念
- Module:模块,包含controller、service等,可以引入其余模块,注入service,导出本模块内容;
- Controller :控制器,用于处理路由,解析请求参数,配置接口地址的地方;
- Service :实现业务逻辑的地方,操作数据库增删改查,返回数据,被controller直接调用;
- Dto :data transfer object,数据传输对象,用于封装请求体里数据的对象,实际就是接口接收的参数ts类型 ,可以在这里配置一些验证条件;
- Entity :对应数据库中的表的实体,这里的字段配置与数据库保持一致,字段类型长度等,创建数据库连接后操作数据库时使用(Repository 对象);
- Ioc:Inverse of Controller,反转控制、依赖注入,只要声明了依赖,nest会自动注入依赖的实例;
- Aop :切面编程,在接口相应流程中可以复用的逻辑 ,在module、controller、service阶段插入一些公共或特殊处理逻辑,具体包含middleware、interceotor、guard、exception filter、pipe;
Ioc
IoC 机制是在 class 上标识哪些是可以被注入的,它的依赖是什么,然后从入口开始扫描这些对象和依赖,自动创建和组装对象。
Nest 里通过 @Controller 声明可以被注入的 controller,通过 @Injectable 声明可以被注入也可以注入别的对象的 provider,然后在 @Module 声明的模块里引入。
并且 Nest 还提供了 Module 和 Module 之间的 import,可以引入别的模块的 provider 来注入。
在 main.ts 里调用 NestFactory.create 方法,就会从 AppModule 开始递归解析 Module,实例化其中的 provider、controller,并依次调用它们的 onModuleInit 生命周期方法。
之后会再递归调用每个 Module 的 provider、controller 的还有 Module 自身的 onApplicationBootstrap 生命周期方法。
Aop
Nest 基于 express 这种 http 平台做了一层封装,应用了 MVC、IOC、AOP 等架构思想。
MVC 就是 Model、View Controller 的划分,请求先经过 Controller,然后调用 Model 层的 Service、Repository 完成业务逻辑,最后返回对应的 View。
IOC 是指 Nest 会自动扫描带有 @Controller、@Injectable 装饰器的类,创建它们的对象,并根据依赖关系自动注入它依赖的对象,免去了手动创建和组装对象的麻烦。
AOP 则是把通用逻辑抽离出来,通过切面的方式添加到某个地方,可以复用和动态增删切面逻辑。
Nest 的 Middleware、Guard、Interceptor、Pipe、ExceptionFilter 都是 AOP 思想的实现,只不过是不同位置的切面,它们都可以灵活的作用在某个路由或者全部路由,这就是 AOP 的优势。
Middleware 是 Express 的概念,在最外层,到了某个路由之后,会先调用 Guard,Guard 用于判断路由有没有权限访问,然后会调用 Interceptor,对 Contoller 前后扩展一些逻辑,在到达目标 Controller 之前,还会调用 Pipe 来对参数做检验和转换。所有的 HttpException 的异常都会被 ExceptionFilter 处理,返回不同的响应。
Nest 就是通过这种 AOP 的架构方式,实现了松耦合、易于维护和扩展的架构。
执行顺序:
request => Middleware => Gurad=> Interceptor before => Pipe => handler => Interceptor after => Exception filter => response
- 请求会被 middleware 处理,这一层可以复用 express 的中间件生态,实现 session、static files 等功能;可middleware可以注入 provider
- 在具体的路由会经历 Guard 的处理,它可以通过 ExecutionContext 拿到目标 class、handler 的metadata 等信息,可以实现权限验证等功能;
- Interceptor 可以在请求前后做一些处理,它也可以通过 ExecutionContext 拿到 class、handler 的信息;
- 在到达 handler 之前,还会对参数用 Pipe 做下检验和转换;
- 这个过程中不管是哪一层抛的异常,都会被 Exception Filter 处理下,返回给用户友好的响应信息;
Nest CLI
bash
npx install -g @nest/cli
nest new 项目名
nest -h
middleware中间件
Middleware是在路由处理程序之前 调用的函数。中间件函数可以访问请求和响应对象,以及应用程序的请求-响应周期中的next()
中间件函数。
中间件函数可以执行以下任务:
- 执行任何代码。
- 对请求和响应对象进行更改。
- 结束请求-响应周期。
- 调用堆栈中的下一个中间件函数。
- 如果当前中间件函数未结束请求-响应周期,则必须调用
next()
将控制权传递给下一个中间件函数。否则,请求将被搁置。
中间件需要实现 NestMiddleware 接口
ts
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();
}
}
// 函数式中间件 - 当中间件不需要任何依赖时,可以考虑使用更简单的 函数式中间件 替代方案。
export function logger(req: Request, res: Response, next: NextFunction) {
console.log(`Request...`);
next();
};
在应用中使用中间件
ts
import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './logger.middleware';
import { UserMiddleware } from './user.middleware';
@Module()
export class AppModule implements NestModule {
// configure()方法可以使用async/await进行异步化
configure(consumer: MiddlewareConsumer) {
consumer
.apply(UserMiddleware, LoggerMiddleware) // 多个中间件, 用逗号隔开
.exclude(
{ path: 'user', method: RequestMethod.POST }, // 排除user路由的POST请求
)
.forRoutes({ path: 'user', method: RequestMethod.GET }); // 针对user路由的GET请求使用中间件, path可以使用通配符, 例如 us*er, 将会匹配usaer, us_er, user等
// .forRoutes(UserController); // 针对UserController的所有路由使用中间件
}
}
全局使用中间件
ts
import { logger } from './logger.middleware';
app.use(logger);
装饰器
@Module, @Global
- Module:声明模块;
- Global:标注此模块为全局模块,他的exports可以不引入直接注入了;
ts
import { Module } from '@nestjs/common';
@Global()
@Module({
// 引入其他模块
imports: [],
// 本模块的controller
controller: [],
// 注入的services
providers: [],
// 导出本模块的services给其他模块使用
exports: []
})
export class AppModule {}
当遇见 A 模块 引入了 B模块,B模块 也引入了 A模块,形成了循环依赖,会出现找不到依赖的情况;
需要使用 forwardRef
ts
/* A Module */
import { Module, forwardRef } from '@nestjs/common';
import { BModule } from '../B.module'
@Module({
imports: [
forwardRef(() => BModule)
]
})
export class AModule {}
/* B Module */
import { Module, forwardRef } from '@nestjs/common';
import { AModule } from '../A.module'
@Module({
imports: [
forwardRef(() => AModule)
]
})
export class BModule {}
@controller, @Injectable, @Inject
- controller:声明controller控制器,路由配置;
- Injectable:声明provider提供者,可以被注入,可通过 Inject 引入使用;
- Inject:注入Injectable提供的依赖;
同模块内的 controller 和 Injectable 声明的内容, 记得在 @Module 那里加入配置;
ts
/* app.service.ts */
import { Injectable } from '@nestjs/common';
// 代表这个可以被注入, 使用@Inject引入
@Injectable()
export class AppService {
getString(): string {
return 'Hello World!'
}
}
ts
/* app.controller.ts */
import { Controller, Inject, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller('app')
export class AppController {
// 构造器注入
/* constructor(
private readonly appService: AppService
) {} */
// 属性注入
@Inject(AppService)
private readonly appService: AppService;
@Get()
getHello(): string {
return this.appService.getString();
}
}
接口参数类
@Param
Param:get请求参数,url地址栏后面的字符串, 比如 url/get/:id 中的 id
ts
@Get(':id')
findOne(@Param('id') id: string) {
return this.taskService.findOne(+id);
}
@Get('ids/:ids')
getUserByIds(
@Param(
'ids',
new ParseIntPipe({
errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE,
}),
)
ids: number,
) {
return {
ids,
};
}
@Query
get请求参数,拼接在地址栏上的参数,比如 /aaa?name=xx 中的 name
ts
@Get('users')
// 通过指定参数名称获取, 可以添加参数处理管道
findUser(@Query('name') name: string, @Query('age', ParseIntPipe) age: string) {
return {}
}
@Get('users')
// 不指定参数,获取所有query参数
// findUser(@Query() query: UserDto) {
findUser(@Query() query: { name: string, age: string }) {
return {}
}
@Body
post,put等请求参数,放在body请求体中,通过 dto class接收
ts
@Post('create')
async create(@Body() book: CreateBookDto) {
return this.bookService.create(book);
}
@Post('create')
// 提取单个字段, 也可以使用管道
async create(@Body('name') name: name) {
return this.bookService.create(name);
}
接口请求方式
@Get, @Post, @Put, @Delete, @Patch, Options, @Head
ts
import {
Body,
Controller,
Delete,
forwardRef,
Get,
Inject,
Param,
Patch,
Post,
} from '@nestjs/common';
import { UserService } from '../user/user.service';
import { CreateTaskDto } from './dto/create-task.dto';
import { UpdateTaskDto } from './dto/update-task.dto';
import { TaskService } from './task.service';
// import { FileSizePipe } from '../../hook/file-size.pipe';
@Controller('task')
export class TaskController {
constructor(
private readonly taskService: TaskService,
// 循环依赖, 使用Inject + forwardRef
@Inject(forwardRef(() => UserService)) private userService: UserService,
) {}
@Post()
create(@Body() createTaskDto: CreateTaskDto) {
return this.taskService.create(createTaskDto);
}
@Get()
findAll() {
return this.taskService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.taskService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateTaskDto: UpdateTaskDto) {
return this.taskService.update(+id, updateTaskDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.taskService.remove(+id);
}
}
接口响应类
@HttpCode
指定接口的返回状态码
ts
@Get('all')
@HttpCode(200)
async findAll() {
return this.bookService.findAll();
}
@Post('create')
@HttpCode(201) // nest中的post成功状态码是201
async create(@Body() book: CreateBookDto) {
return this.bookService.create(book);
}
@Req, @Request
@Res, @Response
@Req
和@Res
装饰器用于直接访问Express.js的请求(Request)和响应(Response)对象。虽然NestJS提供了许多内置装饰器(如@Body
、@Query
等)来简化请求处理,但在某些情况下,你可能需要直接操作原始的请求或响应对象。
注入 response 对象,一旦注入了这个 Nest 就不会把返回值作为响应了, 需要手动 send,除非指定 passthrough 为true
ts
import { Controller, Get, Req } from '@nestjs/common&';
import { Request } from 'express';
@Controller('example')
export class ExampleController {
@Get()
findAll(@Req() request: Request, @Res() response: Response) {
console.log(request.headers);
console.log(request.query);
console.log(request.body);
// ...
response.status(200).send('Hello World!');
}
}
接口其他处理类装饰器
@Version
控制路由接口的版本。
设置了 version 就必须携带版本号了,没带版本号就会访问不到接口;
ts
import { Controller, Get, Version, VERSION_NEUTRAL } from '@nestjs/common';
@Controller({
path: 'user',
version: '1' // 所以接口默认版本为1, 这里声明了,那么请求接口就必须带版本号
})
export class UserController {
@Get()
getUser(':id') {
}
@Version('2') // 指定版本为2
@Get()
getUserV2() {
}
}
需要在main.ts中开启接口版本功能
ts
/* main.ts */
app.enableVersioning({
type: VersioningType.HEADER, // URI: 在地址上添加版本; CUSTOM: 自定义
header: 'version', // 指定需要携带的字段名
});
类型:VersioningType
-
URI :在接口地址上添加版本
-
HEADER :在请求头添加字段指定版本
- 请求头: version : 2
-
MEDIA_TYPE :在 accept 的 header 里携带版本号
- 请求头: Accept : application/json;version=2
-
CUSTOM :自定义
-
第二个参数接收为一个函数,能拿到 requeset对象
tsconst extractor = (request: Request)=> { if(request.headers['disable-custom']) { return ''; } return request.url.includes('guang') ? '2' : '1'; } app.enableVersioning({ type: VersioningType.CUSTOM, extractor })
-
没带版本号 可以使用 VERSION_NEUTRAL
常量,带的版本号是几都能访问,但是只能按代码顺序访问到第一个匹配的接口;
标记为 VERSION_NEUTRAL
后,其他版本的接口需要提出来放到其他的controller中, 例: UserV2Controller , v2版本要放前面
ts
/* controller.ts */
import { Controller, Get, Version, VERSION_NEUTRAL } from '@nestjs/common';
@Controller({
path: 'user',
version: VERSION_NEUTRAL, // 使用VERSION_NEUTRAL常量,不带版本,所有版本,都可以访问
})
export class UserController {
@Get()
getUser(':id') {
}
@Version('2') // 指定版本为2
@Get()
getUserV2() {
}
}
/* module.ts */
@Module({
controllers: [UserV2Controller, UserController]
})
export class userModule {}
@SetMetadata
@SetMetadata
装饰器用于在类或方法上设置自定义的元数据。这些元数据可以在运行时通过反射机制(如Reflect
对象)获取,从而实现自定义的逻辑或行为。@SetMetadata
通常与自定义装饰器或守卫(Guards)和拦截器(Interceptors)结合使用。
ts
import { SetMetadata } from '@nestjs/common';
// 设置元数据
@SetMetadata('role', 'admin')
@Get('admin')
findAllAdmin() {
// ...
}
获取自定义元数据 :在守卫、拦截器或其他地方使用Reflect
对象获取元数据。
ts
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 roles = this.reflector.get<string[]>('role', context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
return roles.includes(request.user.role);
}
}
ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
constructor(private reflector: Reflector) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const logLevel = this.reflector.get<string>('logLevel', context.getHandler());
console.log(`Logging level: ${logLevel}`);
return next.handle();
}
}
@Headers
取出某个或全部请求头
ts
import { Controller, Get, Headers } from '@nestjs/common';
@Controller('example')
export class ExampleController {
@Get()
findAll(@Headers() headers: Record<string, string | string[]>, @Headers('authorization') authorization: string) {
console.log(headers);
// headers 是一个包含所有请求头的对象
}
}
@Ip
拿到请求的 ip地址
ts
@Get('ip')
ip(@Ip() ip: string) {
// ip 是客户端的IP地址
console.log(ip);
}
@Session
取出 session 对象,但是需要启用 express-session 中间件
ts
@Get('session')
session(@Session() session) {
console.log(session);
}
安装 中间件
bash
npm install express-session
在main.ts中引入并启用
ts
import * as session from 'express-session';
app.use(session({
secret: 'secret', // 加密密钥
cookie: { maxAge: 10000 } // cookie存活时间
}))
启用之后,调用接口会返回一个 set-cookie 的响应头,设置了 cookie,包含 sid 也就是 sesssionid。
之后每次请求都会自动带上这个 cookie;
@HostParam
取出 host 里的参数
ts
import { Controller, Get, HostParam } from '@nestjs/common';
// 只有 host 满足 xx.0.0.1 的时候才会路由到这个 controller。
@Controller({ host: ':host.0.0.1', path: 'aaa' })
export class AaaController {
@Get('bbb')
hello(@HostParam('host') host) {
return host;
}
}
@Catch, @UseFilters
- Catch:用于过滤器,处理抛出的未捕获的异常;声明 filter 过滤器处理的 exception 类型,搭配 BaseExceptionFilter 类型使用,需实现一个 catch 方法;
- UseFilters:使用过滤器,可在 controller 和 handler 层使用;
filter异常过滤器
ts
/* http-exception.filter.ts */
import {
Catch,
HttpException,
ArgumentsHost,
HttpStatus,
} from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
import { Response, Request } from 'express';
/**
* @description 异常过滤器, 捕获所有类型的 错误和异常,并返回一个对用户友好的响应。
*/
@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
// 调用父类的catch方法处理异常
super.catch(exception, host);
// const { httpAdapter } = this.httpAdapterHost; // 获取http适配器
const ctx = host.switchToHttp(); // 获取请求上下文
const response = ctx.getResponse<Response>(); // 获取响应对象
const request = ctx.getRequest<Request>(); // 获取请求对象
/* 判断错误类型是哪一类的,根据不同情况做不同的处理 */
// 获取异常状态码
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let resData = {};
if (exception instanceof HttpException) {
status = exception.getStatus();
resData = exception.getResponse();
}
const resBody = {
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url, // 请求路径
// path2: httpAdapter.getRequestUrl(ctx.getRequest()), // 请求路径
resData: resData, // 响应数据
};
if (![HttpStatus.OK, HttpStatus.CREATED].includes(status)) return;
/*
这里可以根据需要,做一些判断,返回不同的错误相应内容
也可以在这里设置sentry上报错误
*/
response
.status(status) // 设置响应状态码
.json(resBody); // 设置响应内容
}
}
使用异常过滤器
可以放在handle层单独针对某个方法使用,也可以直接放在 @Controller层,对整个控制器的所有方法都应用
ts
/* app.controler.ts */
......
import { AllExceptionsFilter } from './http-exception.filter';
import { UseFilters, HttpException, HttpSatus } from '@nestjs/common';
......
@Get()
@UseFilters(AllExceptionsFilter)
getHello(): string {
throw new HttpException('filter exception', HttpSatus.BAD_REQUEST);
// return this.appService.getString();
}
全局使用在 main.ts
ts
import { HttpAdapterHost, NestFactory } from '@nestjs/core';
import { AllExceptionsFilter } from './http-exception.filter';
...
const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new AllExceptionsFilter(httpAdapter));
@UsePipes
UsePipes:管道,具有转换、验证两个作用,应实现 PipeTransform 接口;
- 转换:管道将输入数据转换为所需的数据输出(例如,将字符串转换为整数)
- 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常
在这两种情况下, 管道 参数(arguments)
会由控制器(controllers)的路由处理程序 进行处理,Nest自带很多开箱即用的内置管道。
ValidationPipe
ParseIntPipe
ParseFloatPipe
ParseBoolPipe
ParseArrayPipe
ParseUUIDPipe
ParseEnumPipe
DefaultValuePipe
ParseFilePipe
现在比较少用接口管道,@UsePipes(new ValidationPipe()),直接使用参数管道 @Body(ValidationPipe) body: Dto 更为实用
在 Dto 中使用 class-validate 可以直接添加各类参数校验条件
pipe管道
ts
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException,
} from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';
import { ObjectSchema } from 'joi';
/**
* @description 管道pipe
* 管道是具有 @Injectable() 装饰器的类。管道应实现 PipeTransform 接口。
* 两个作用:
* 转换:管道将输入数据转换为所需的数据输出(例如,将字符串转换为整数)
* 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常
*/
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value, { 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): boolean {
const types = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
nest有内置许多管道
ts
import { ParseIntPipe, DefaultValuePipe, ValidationPipe, Body } from '@nestjs/common';
@Get(':id')
getUserById(
// 使用参数管道,DefaultValuePipe 赋值默认值,ParseIntPipe将参数转为int类型,可以继续在后面添加自定义管道
@Param('id', new DefaultValuePipe('1'), ParseIntPipe) id: number,
) {
return {
id,
};
}
@Post('get')
// new ValidationPipe() 搭配 dto 里面设置的 class-validator, 可以实现body参数类型校验
postList(@Body(new ValidationPipe()) body: Dto) {
}
全局使用
ts
import { ValidationPipe } from '@nestjs/common';
// 全局管道
app.useGlobalPipes(new ValidationPipe({ transform: true })); // transform 将请求参数转换为对应的dto实体类
@UseGuards
UseGuards:守卫,具有单一职责,用于 权限、角色判断处理指定请求,授权;
守卫在所有中间件之后执行,但在拦截器或管道之前执行。
guard守卫
需要实现 canActivate 方法
ts
/* jwt.guard.ts */
import {
CanActivate,
ExecutionContext,
Inject,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Observable } from 'rxjs';
@Injectable()
export class JwtGuard implements CanActivate {
@Inject()
private readonly jwtService: JwtService;
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
const token = request.headers.authorization?.split(' ');
// 这里做一些权限,角色判断
if (!token || token.length < 2) {
throw new UnauthorizedException('token错误');
}
// 用户是否有权限调用
try {
const decoded = this.jwtService.verify(token[1]);
request.user = decoded.user;
return true;
} catch {
throw new UnauthorizedException('token失效');
}
}
}
在handle方法使用
ts
/* app.controller.ts */
......
import { JwtGuard } from './jwt.guard.ts';
import { UseGuards, HttpException, HttpSatus } from '@nestjs/common';
......
@Get()
@UseGuards(AllExceptionsFilter)
getHello(): string {
throw new HttpException('filter exception', HttpSatus.BAD_REQUEST);
// return this.appService.getString();
}
全局使用
ts
import { JwtGuard } from './jwt.guard.ts';
// 全局守卫
app.useGlobalGuards(new AuthGuard());
@UseInterceptors
拦截器是使用 @Injectable()
装饰器注解的类。拦截器应该实现 NestInterceptor
接口。
- 在函数执行之前/之后绑定额外的逻辑
- 转换从函数返回的结果
- 转换从函数抛出的异常
- 扩展基本函数行为
- 根据所选条件完全重写函数 (例如, 缓存目的)
ts
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`)),
// 响应拦截,返回新的响应对象
// map(value => value === null ? '' : value )
map(data => ({ data })) // 加一层data对象
);
}
}
由于handle()
返回一个RxJS Observable
,我们可以选择多种操作符来操控流。在上面的示例中,我们使用了tap()
操作符,它在可观察流的正常或异常终止时调用我们的匿名日志函数,但不会干扰响应循环。
ts
// 使用拦截器
@UseInterceptors(LoggingInterceptor)
export class UserController {
// handle级别的拦截器
// @UseInterceptors(LoggingInterceptor)
getAll() {
}
}
在调用 UserController
下的路由时,就会看到 LoggingInterceptor
拦截器中的 console.log()
@Render
指定渲染用的模版引擎
文件上传
通过 Express 的 Multer 包处理 multipart/form-data 类型的请求中的 file;
提供了 FileInterceptor、FilesInterceptor、FileFieldsInterceptor、AnyFilesInterceptor 的拦截器
安装 multer 的 ts 类型包
bash
npm i -D @types/multer
Main.ts 开启 express 底层功能,开启跨域,支持静态文件的访问;
ts
import { HttpAdapterHost, NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { WINSTON_LOGGER_TOKEN } from './modules/winston/winston.module';
async function bootstrap() {
// NestExpressApplication 使用express底层功能
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// 跨域
app.enableCors();
// 设置uploads为静态文件目录, useStaticAssets 要开启NestExpressApplication
app.useStaticAssets(join(__dirname, '../uploads'), { prefix: '/uploads' });
await app.listen(1111);
}
bootstrap()
以下文件上传都是将文件存在本地目录,正常需要存到服务器,或是oss 阿里云 腾讯云 对象存储的方式 的,后面再介绍。
单文件上传
通过 FileInterceptor
拦截器解析请求里的 file
字段,第二个参数是 options
配置
ts
import {
Post,
Body,
UseInterceptors,
BadRequestException,
UploadedFile,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { mkdirSync } from 'fs';
import { extname } from 'path';
const diskStorage = diskStorage({
// 确定文件上传后存放的文件夹
destination: function (req, file, cb) {
try {
// 创建文件夹, 已存在会报错
mkdirSync('uploads');
} catch (e) {
console.log(e);
}
cb(null, 'uploads');
},
// 生成文件名
filename: function (req, file, cb) {
const uniqieSuffix =
Date.now() +
'-' +
Math.round(Math.random() * 1e9) +
'-' +
file.originalname;
cb(null, uniqieSuffix);
},
});
@Post('upload')
@UseInterceptors(
FileInterceptor('file', {
dest: 'uploads', // 文件存放目录
storage: diskStorage,
limits: { fileSize: 1024 * 1024 * 5 }, // 5MB
fileFilter(req, file, cb) {
const extName = extname(file.originalname);
if (['.png', '.jpg', '.jpeg', '.gif'].includes(extName)) {
cb(null, true);
} else {
cb(new BadRequestException('Only image files are allowed'), false);
}
},
}),
)
async upload(@UploadedFile() file: Express.Multer.File) {
console.log('file', file);
return file.path;
}
多文件上传,大文件上传
前端将文件切片上传,分片文件上传完后,调用合并接口将切片文件合并成原始文件
ts
const chunkSize = 20 * 1024; // 分片大小
const fileInput = document.querySelector('#fileInput');
async function chunkFile() {
const file = fileInput.files[0];
const chunks = []
let startPos = 0;
// 将文件切片储存到chunks数组中
while (startPos < file.size) {
chunks.push(file.slice(startPos, startPos + chunkSize));
startPos += chunkSize;
}
const task = []
// 添加随机字符串防止切片文件名重复
const randomStr = Math.random().toString().slice(2, 8)
// 将切片上传到服务器
chunks.map((item, index) => {
const data = new FormData();
// 在file对象添加 分片文件名,文件名携带分片索引,总分片数
data.set('name', `${randomStr}_${file.name}-${index}`);
data.set('total', chunks.length);
data.append('file', item);
task.push(axios.post('http://localhost:1111/book/upload-big-files', data))
})
await Promise.all(task)
// 分片文件上传完后,调用合并接口将切片文件合并成原始文件
axios.get('http://localhost:1111/book/merge?name=' + randomStr + '_' + file.name)
}
fileInput.onchange = chunkFile;
后端接收传来的多个 file 文件
ts
import {
Post,
Body,
UseInterceptors,
UploadedFiles,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { mkdirSync, existsSync, cpSync, rmSync } from 'fs';
import { extname } from 'path';
@Post('upload-big-files')
@UseInterceptors(FilesInterceptor('file', 20, { dest: 'uploads' }))
async uploadBigFiles(
// 单文件这里是不带s的
@UploadedFiles() files: Express.Multer.File[],
@Body() body,
) {
console.log('files', files);
console.log('body', body);
// 匹配文件名,去掉文件名末尾 '-' 后面的数字部分
const fileName = body.name.match(/(.+)\-\d+$/)[1];
// 生成分片文件储存目录
const chunkDir = 'uploads/chunks_' + fileName;
// existsSync 检查文件目录是否存在
if (!existsSync(chunkDir)) {
mkdirSync(chunkDir);
}
// 将分片文件复制到目录中
cpSync(files[0].path, chunkDir + '/' + body.name);
// 删除原始上传的分片文件,节省存储空间
rmSync(files[0].path);
}
合并分片文件接口
ts
import {
Get,
Query
} from '@nestjs/common';
import { readdirSync, createReadStream, createWriteStream, statSync, rmSync } from 'fs';
import { extname } from 'path';
@Get('merge')
merge(@Query('name') name: string) {
// 分片文件所在目录
const chunkDir = 'uploads/chunks_' + name;
// 读取目录下的文件
const files = readdirSync(chunkDir);
let startPos = 0;
let count = 0;
// 便利分片文件
files.map((file) => {
const filePath = chunkDir + '/' + file;
const stream = createReadStream(filePath);
// 将分片文件写入到合并文件中
stream
.pipe(
createWriteStream('uploads/' + name, {
start: startPos,
}),
)
.on('finish', () => {
count++;
if (count === files.length) {
// 合并完成,删除分片文件目录
rmSync(chunkDir, { recursive: true, force: true });
}
});
startPos += statSync(filePath).size;
});
}
OSS
上传文件一般不会直接存在服务器目录下,一般会用阿里云的 OSS,它会自己做弹性扩展,所以存储空间是无限的。
OSS 对象存储是在一个 bucket 桶下,存放多个文件。
它是用 key-value 存储的,没有目录的概念,阿里云 OSS 的目录只是用元信息来模拟实现的。
不管在哪里上传,都需要 acessKeyId 和 acessKeySecret。
这个是阿里云的安全策略,因为直接用用户名密码,一旦泄漏就很麻烦,而 acessKey 泄漏了也可以禁用。而且建议用 RAM 子用户的方式生成 accessKey,这样可以最小化权限,进一步减少泄漏的风险。
客户端直传 OSS 的方式不需要消耗服务器的资源,但是会有泄漏 acessKey 的风险,所以一般都是用服务端生成临时的签名等信息,然后用这些信息来上传。
这种方案就是最完美的 OSS 上传方案了。
。。。待实现。
Nest Winston
记录日志
数据库集成
TypeORM
TypeORM 是 NestJS 最常用的 ORM,支持多种数据库。
安装和配置
bash
npm install @nestjs/typeorm typeorm mysql2
# 或使用其他数据库驱动
npm install @nestjs/typeorm typeorm pg
npm install @nestjs/typeorm typeorm sqlite3
基本配置
ts
// app.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'password',
database: 'test',
entities: [User, Task], // 实体类
synchronize: true, // 开发环境自动同步数据库结构
logging: true, // 打印SQL语句
}),
],
})
export class AppModule {}
实体定义
ts
// user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
username: string;
@Column()
email: string;
@Column({ select: false }) // 默认查询时不返回密码
password: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
Repository 模式
ts
// user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
) {}
async findAll(): Promise<User[]> {
return this.userRepository.find();
}
async findOne(id: number): Promise<User> {
return this.userRepository.findOne({ where: { id } });
}
async create(userData: Partial<User>): Promise<User> {
const user = this.userRepository.create(userData);
return this.userRepository.save(user);
}
async update(id: number, userData: Partial<User>): Promise<User> {
await this.userRepository.update(id, userData);
return this.findOne(id);
}
async remove(id: number): Promise<void> {
await this.userRepository.delete(id);
}
}
关系映射
ts
// 一对多关系
@Entity('users')
export class User {
@OneToMany(() => Task, task => task.user)
tasks: Task[];
}
@Entity('tasks')
export class Task {
@ManyToOne(() => User, user => user.tasks)
@JoinColumn({ name: 'user_id' })
user: User;
}
// 多对多关系
@Entity('users')
export class User {
@ManyToMany(() => Role, role => role.users)
@JoinTable()
roles: Role[];
}
@Entity('roles')
export class Role {
@ManyToMany(() => User, user => user.roles)
users: User[];
}
Prisma
Prisma 是另一个流行的 ORM 选择。
安装和配置
bash
npm install prisma @prisma/client
npm install -D prisma
npx prisma init
Schema 定义
prisma
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Service 使用
ts
// user.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Injectable()
export class UserService {
constructor(private prisma: PrismaService) {}
async findAll() {
return this.prisma.user.findMany({
include: { posts: true }
});
}
async create(data: { email: string; name?: string }) {
return this.prisma.user.create({ data });
}
}
配置管理
环境变量
使用 @nestjs/config
包管理配置。
bash
npm install @nestjs/config
ts
// app.module.ts
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // 全局可用
envFilePath: ['.env.local', '.env'], // 环境变量文件路径
}),
],
})
export class AppModule {}
ts
// .env
DATABASE_URL=mysql://root:password@localhost:3306/test
JWT_SECRET=your-secret-key
PORT=3000
ts
// 使用配置
import { ConfigService } from '@nestjs/config';
@Injectable()
export class DatabaseService {
constructor(private configService: ConfigService) {}
getDatabaseUrl(): string {
return this.configService.get<string>('DATABASE_URL');
}
}
配置验证
ts
// config/validation.schema.ts
import * as Joi from 'joi';
export const validationSchema = Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'production', 'test')
.default('development'),
PORT: Joi.number().default(3000),
DATABASE_URL: Joi.string().required(),
JWT_SECRET: Joi.string().required(),
});
认证和授权
JWT 认证
bash
npm install @nestjs/jwt @nestjs/passport passport passport-jwt
npm install -D @types/passport-jwt
ts
// auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
PassportModule,
JwtModule.register({
secret: 'your-secret-key',
signOptions: { expiresIn: '1h' },
}),
],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
ts
// auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(private jwtService: JwtService) {}
async validateUser(username: string, password: string): Promise<any> {
// 验证用户逻辑
const user = await this.findUserByUsername(username);
if (user && user.password === password) {
const { password, ...result } = user;
return result;
}
return null;
}
async login(user: any) {
const payload = { username: user.username, sub: user.id };
return {
access_token: this.jwtService.sign(payload),
};
}
}
ts
// jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: 'your-secret-key',
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
角色权限
ts
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
ts
// roles.guard.ts
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<string[]>('roles', [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
缓存
Redis 缓存
bash
npm install @nestjs/cache-manager cache-manager cache-manager-redis-store
npm install -D @types/cache-manager
ts
// app.module.ts
import { CacheModule } from '@nestjs/cache-manager';
import { redisStore } from 'cache-manager-redis-store';
@Module({
imports: [
CacheModule.register({
store: redisStore,
host: 'localhost',
port: 6379,
ttl: 300, // 默认过期时间(秒)
}),
],
})
export class AppModule {}
ts
// user.service.ts
import { Injectable, Inject, CACHE_MANAGER } from '@nestjs/common';
import { Cache } from 'cache-manager';
@Injectable()
export class UserService {
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
async findOne(id: number) {
const cacheKey = `user_${id}`;
let user = await this.cacheManager.get(cacheKey);
if (!user) {
user = await this.userRepository.findOne({ where: { id } });
await this.cacheManager.set(cacheKey, user, 300); // 缓存5分钟
}
return user;
}
async update(id: number, userData: Partial<User>) {
const result = await this.userRepository.update(id, userData);
await this.cacheManager.del(`user_${id}`); // 清除缓存
return result;
}
}
任务调度
Cron 任务
bash
npm install @nestjs/schedule
ts
// app.module.ts
import { ScheduleModule } from '@nestjs/schedule';
@Module({
imports: [ScheduleModule.forRoot()],
})
export class AppModule {}
ts
// tasks.service.ts
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
@Injectable()
export class TasksService {
@Cron('45 * * * * *') // 每分钟的第45秒执行
handleCron() {
console.log('Called when the current second is 45');
}
@Cron(CronExpression.EVERY_30_SECONDS) // 每30秒执行
handleInterval() {
console.log('Called every 30 seconds');
}
@Cron('0 0 0 * * *') // 每天午夜执行
handleDaily() {
console.log('Called daily at midnight');
}
}
微服务
TCP 微服务
bash
npm install @nestjs/microservices
ts
// main.ts
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.TCP,
options: {
host: 'localhost',
port: 3001,
},
},
);
await app.listen();
}
bootstrap();
ts
// user.controller.ts
import { Controller } from '@nestjs/common';
import { MessagePattern, EventPattern } from '@nestjs/microservices';
@Controller()
export class UserController {
@MessagePattern('get_user')
getUser(data: { id: number }) {
return { id: data.id, name: 'John Doe' };
}
@EventPattern('user_created')
handleUserCreated(data: { id: number; name: string }) {
console.log('User created:', data);
}
}
RabbitMQ 微服务
bash
npm install @nestjs/microservices amqplib
npm install -D @types/amqplib
ts
// main.ts
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.RMQ,
options: {
urls: ['amqp://localhost:5672'],
queue: 'user_queue',
queueOptions: {
durable: false,
},
},
},
);
测试
单元测试
ts
// user.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './user.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from './user.entity';
describe('UserService', () => {
let service: UserService;
let mockRepository = {
find: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
{
provide: getRepositoryToken(User),
useValue: mockRepository,
},
],
}).compile();
service = module.get<UserService>(UserService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should return all users', async () => {
const mockUsers = [{ id: 1, name: 'John' }];
mockRepository.find.mockResolvedValue(mockUsers);
const result = await service.findAll();
expect(result).toEqual(mockUsers);
expect(mockRepository.find).toHaveBeenCalled();
});
});
E2E 测试
ts
// app.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});
性能优化
压缩响应
bash
npm install compression
ts
// main.ts
import * as compression from 'compression';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(compression());
await app.listen(3000);
}
限流
bash
npm install @nestjs/throttler
ts
// app.module.ts
import { ThrottlerModule } from '@nestjs/throttler';
@Module({
imports: [
ThrottlerModule.forRoot({
ttl: 60,
limit: 10,
}),
],
})
export class AppModule {}
ts
// user.controller.ts
import { Throttle } from '@nestjs/throttler';
@Controller('user')
export class UserController {
@Throttle(5, 60) // 每分钟最多5次请求
@Get()
findAll() {
return this.userService.findAll();
}
}
PM2
日志管理、进程管理、负载均衡、性能监控、服务重启
安装和配置
bash
npm install -g pm2
json
// ecosystem.config.js
module.exports = {
apps: [{
name: 'nestjs-app',
script: 'dist/main.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'development',
PORT: 3000
},
env_production: {
NODE_ENV: 'production',
PORT: 3000
},
log_file: './logs/combined.log',
out_file: './logs/out.log',
error_file: './logs/error.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
}]
};
常用命令
bash
# 启动应用
pm2 start ecosystem.config.js
# 启动生产环境
pm2 start ecosystem.config.js --env production
# 查看状态
pm2 status
# 查看日志
pm2 logs
# 重启应用
pm2 restart nestjs-app
# 停止应用
pm2 stop nestjs-app
# 删除应用
pm2 delete nestjs-app
# 监控
pm2 monit
生命周期钩子
NestJS 提供了多个生命周期钩子,让你可以在应用启动和关闭时执行特定逻辑。
应用生命周期
ts
// app.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy, OnApplicationBootstrap, OnApplicationShutdown } from '@nestjs/common';
@Injectable()
export class AppService implements OnModuleInit, OnModuleDestroy, OnApplicationBootstrap, OnApplicationShutdown {
onModuleInit() {
console.log('Module initialized');
}
onApplicationBootstrap() {
console.log('Application bootstrapped');
}
onModuleDestroy() {
console.log('Module destroyed');
}
onApplicationShutdown(signal?: string) {
console.log('Application shutdown', signal);
}
}
模块生命周期
ts
// app.module.ts
import { Module, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
@Module({
// ...
})
export class AppModule implements OnModuleInit, OnModuleDestroy {
onModuleInit() {
console.log('AppModule initialized');
}
onModuleDestroy() {
console.log('AppModule destroyed');
}
}
WebSocket
安装和配置
bash
npm install @nestjs/websockets @nestjs/platform-socket.io
npm install -D @types/socket.io
ts
// app.module.ts
import { Module } from '@nestjs/common';
import { ChatGateway } from './chat.gateway';
@Module({
providers: [ChatGateway],
})
export class AppModule {}
Gateway 实现
ts
// chat.gateway.ts
import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
MessageBody,
ConnectedSocket,
OnGatewayConnection,
OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway({
cors: {
origin: '*',
},
})
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
handleConnection(client: Socket) {
console.log(`Client connected: ${client.id}`);
}
handleDisconnect(client: Socket) {
console.log(`Client disconnected: ${client.id}`);
}
@SubscribeMessage('join_room')
handleJoinRoom(
@MessageBody() data: { room: string },
@ConnectedSocket() client: Socket,
) {
client.join(data.room);
this.server.to(data.room).emit('user_joined', {
message: `User ${client.id} joined room ${data.room}`,
});
}
@SubscribeMessage('send_message')
handleMessage(
@MessageBody() data: { room: string; message: string },
@ConnectedSocket() client: Socket,
) {
this.server.to(data.room).emit('new_message', {
id: client.id,
message: data.message,
timestamp: new Date(),
});
}
}
客户端连接
html
<!-- 前端 HTML -->
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
<script>
const socket = io('http://localhost:3000');
socket.on('connect', () => {
console.log('Connected to server');
});
socket.emit('join_room', { room: 'general' });
socket.on('new_message', (data) => {
console.log('New message:', data);
});
function sendMessage() {
socket.emit('send_message', {
room: 'general',
message: 'Hello World!'
});
}
</script>
GraphQL
安装和配置
bash
npm install @nestjs/graphql @nestjs/apollo @apollo/server graphql
ts
// app.module.ts
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
sortSchema: true,
}),
],
})
export class AppModule {}
Resolver 实现
ts
// user.resolver.ts
import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql';
import { UserService } from './user.service';
import { User } from './user.entity';
import { CreateUserInput } from './dto/create-user.input';
@Resolver(() => User)
export class UserResolver {
constructor(private userService: UserService) {}
@Query(() => [User])
users(): Promise<User[]> {
return this.userService.findAll();
}
@Query(() => User, { nullable: true })
user(@Args('id', { type: () => Int }) id: number): Promise<User> {
return this.userService.findOne(id);
}
@Mutation(() => User)
createUser(@Args('createUserInput') createUserInput: CreateUserInput): Promise<User> {
return this.userService.create(createUserInput);
}
}
健康检查
安装和配置
bash
npm install @nestjs/terminus
ts
// health.controller.ts
import { Controller, Get } from '@nestjs/common';
import {
HealthCheckService,
HealthCheck,
TypeOrmHealthIndicator,
MemoryHealthIndicator,
} from '@nestjs/terminus';
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private db: TypeOrmHealthIndicator,
private memory: MemoryHealthIndicator,
) {}
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.db.pingCheck('database'),
() => this.memory.checkHeap('memory_heap', 150 * 1024 * 1024),
]);
}
}
文档生成
Swagger 配置
bash
npm install @nestjs/swagger swagger-ui-express
ts
// main.ts
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('API Documentation')
.setDescription('The API description')
.setVersion('1.0')
.addTag('users')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
await app.listen(3000);
}
bootstrap();
最佳实践
项目结构
arduino
src/
├── app.module.ts
├── main.ts
├── common/
│ ├── decorators/
│ ├── filters/
│ ├── guards/
│ ├── interceptors/
│ └── pipes/
├── config/
├── modules/
│ ├── auth/
│ │ ├── auth.controller.ts
│ │ ├── auth.service.ts
│ │ ├── auth.module.ts
│ │ ├── dto/
│ │ └── strategies/
│ ├── users/
│ │ ├── users.controller.ts
│ │ ├── users.service.ts
│ │ ├── users.module.ts
│ │ ├── dto/
│ │ └── entities/
│ └── database/
│ ├── database.module.ts
│ └── database.service.ts
└── utils/
响应拦截器
ts
// common/interceptors/transform.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response<T> {
data: T;
code: number;
message: string;
timestamp: string;
}
@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,
code: 200,
message: 'success',
timestamp: new Date().toISOString(),
})),
);
}
}
日志记录
ts
// common/interceptors/logging.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
Logger,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url, body } = request;
const now = Date.now();
return next.handle().pipe(
tap(() => {
const response = context.switchToHttp().getResponse();
const { statusCode } = response;
const contentLength = response.get('content-length');
this.logger.log(
`${method} ${url} ${statusCode} ${contentLength} - ${Date.now() - now}ms`,
);
}),
);
}
}