NestJS
是一个完全支持 TypeScript
的 Node.js
服务端开发框架,其基本工作原理是在常见的框(Express/Fastify)上重新抽象了一个级别,通过装饰器串联起各工作模块。整个工作流程主要由以下等模块组成:
Controller
(控制器)
Provider
(提供者)
Module
(模块)
Middleware
(中间件)
Filter
(过滤器)
Pipe
(管道)
Guard
(守卫)
Decorator
(装饰器)
模块间的关系(生命周期)如图所示:

下面以一个demo对各个模块进行简单的介绍,项目结构如下:
├── src
| ├── app.module.ts
| ├── main.ts
| ├── filter
| | └── http.filter.ts
| ├── guard
| | └── auth.guard.ts
| ├── interceptor
| | └── app.interceptor.ts
| ├── middleware
| | └── proxy.middleware.ts
| ├── modules
| | ├── demo
| | | ├── demo.controller.ts
| | | ├── demo.dto.ts
| | | ├── demo.module.ts
| | | └── demo.service.ts
| ├── pipe
| | └── validation.pipe.ts
Controller
负责处理
http
接口请求和响应,可以理解为对应各路由 API 路径处理。/* demo.controller.ts */
import { Controller, Get, Query, Post, Body } from '@nestjs/common';
@Controller('/demo')
export class DemoController {
@Get('/say')
async sayGet(@Query() query) {
return 'hello world';
}
@Post('/say')
async sayPost(@Body() body) {
return 'hello world';
}
}
其中对应的
@Query
、@Body
分别获取 Get 和 Post 请求的参数。Provider
一个用
@Injectable()
装饰器包装的类,可以通过 constructor
注入依赖关系,其实就是传统的Service服务,用来处理数据相关。/* demo.service.ts */
import { Injectable } from '@nestjs/common'
@Injectable()
export class DemoService {
constructor() {}
async getName(): Promise<string> {
return 'onfuns'
}
}
然后在
demo.controller.ts
注入service类。import { Inject, Controller, Get, Query, Post, Body } from '@nestjs/common'
import { DemoService } from './demo.service'
@Controller('/demo')
export class DemoController {
constructor(@Inject(DemoService) private readonly service: DemoService) {}
@Get('/say')
async sayGet(@Query() query) {
console.log(query)
const name = await this.service.getName()
return `hello sayGet ${name}`
}
@Post('/say')
async sayPost(@Body() body) {
console.log(body)
return 'hello sayPost'
}
}
此时 provider 已经准备就绪,只要在module中完成关联就可以使用了。
Module
聚合控制器和提供者的类,由
@Module
包裹,一个项目有多个模块,每个模块都有紧密相关的功能。当其他模块需要引入时,可以通过 exports
暴露出Service供其他功能使用。/* demo.module.ts */
import { Module } from '@nestjs/common'
import { DemoController } from './demo.controller'
import { DemoService } from './demo.service'
@Module({
imports: [],
controllers: [DemoController],
providers: [DemoService],
exports:[]
})
export class DemoModule {}
Middleware
中间件是在路由处理前调用,等用于
express
中间件功能,下面使用中间件对一些特定的路由代理。/* proxy.middleware.ts */
import { createProxyMiddleware } from 'http-proxy-middleware'
import { NestMiddleware } from '@nestjs/common'
import { Request, Response } from 'express'
export class ProxyMiddleware implements NestMiddleware {
private proxy = createProxyMiddleware({
target: 'https://api.com',
changeOrigin: true,
pathRewrite: {
'/api': '/api/v2',
},
secure: false,
onProxyReq: (proxyReq, req: Request, res: Response) => {
console.log(
`[NestMiddleware]: Proxying ${req.url} request originally made to '${req.originalUrl}'...`,
)
},
onProxyRes: (proxyReq, req: Request, res: Response) => {},
})
use(req: Request, res: Response, next: () => void) {
this.proxy(req, res, next)
}
}
在根 module 中注册使用
/* app.module.ts */
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common'
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(ProxyMiddleware).forRoutes({ path: 'api/*', method: RequestMethod.ALL })
}
}
Filter
内置的异常捕获器,负责处理整个应用程序中的所有抛出的异常。当捕获到未处理的异常时,可以转换为友好的方式让客户端使用,以下为返回一个统一结构的错误响应数据。
/* http.filter.ts */
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common'
import { Request, Response } from 'express'
@Catch()
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 instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR
const success = status >= 200 && status < 300
if (!success) console.log(exception)
const errorResponse = {
success,
data: null,
message: exception.message,
}
response.status(status)
response.header('Content-Type', 'application/json; charset=utf-8')
response.send(errorResponse)
}
}
Pipe
管道对传入数据转换或校验,一般结合Dto使用,可以校验用户传入数据的准确性:
- 转换:管道将输入数据转换为所需的数据输出;
- 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常。
/* validation.pipe.ts */
import { ArgumentMetadata, PipeTransform, HttpException, HttpStatus } from '@nestjs/common'
import { validate } from 'class-validator'
import { plainToClass } from 'class-transformer'
export class ValidationPipe implements PipeTransform {
async transform(value: any, { metatype }: ArgumentMetadata) {
// 如果没有传入验证规则,则不验证,直接返回数据
if (!metatype || !this.toValidate(metatype)) {
return value
}
const object = plainToClass(metatype, value)
const errors = await validate(object)
if (errors.length > 0) {
const msg = Object.values(errors[0].constraints)[0]
throw new HttpException(`failed: ${msg}`, HttpStatus.BAD_REQUEST)
}
return value
}
private toValidate(metatype: any): boolean {
const types: any[] = [String, Boolean, Number, Array, Object]
return !types.includes(metatype)
}
}
创建需要的dto文件,通过
class-validator
辅助验证数据的有效性。/* demo.dto.ts */
import { IsNotEmpty } from 'class-validator'
export class CreateDto {
@IsNotEmpty({ message: '名称不能为空' })
name: string
@IsNotEmpty({ message: '分类不能为空' })
category_id: string
}
export class UpdateDto {
@IsNotEmpty({ message: 'id 不能为空' })
id: number
}
在controller中将dto 与对应的入参绑定,当入参不符合dto中的规则时会抛出异常。
/* demo.controller.ts */
import { Inject, Controller, Get, Query, Post, Body } from '@nestjs/common'
import { DemoService } from './demo.service'
import { CreateDto, UpdateDto } from './demo.dto'
@Controller('/demo')
export class DemoController {
constructor(@Inject(DemoService) private readonly service: DemoService) {}
@Get('/say')
async sayGet(@Query() query: UpdateDto) {
console.log(query)
const name = await this.service.getName()
return `hello sayGet ${name}`
}
@Post('/say')
async sayPost(@Body() body: CreateDto) {
console.log(body)
return 'hello sayPost'
}
}
Guard
守卫根据权限,角色,访问控制列表等来给某些路由加上特定权限,一下为一个全局守卫,会判断roles 元数据,当 roles 为 all 时则不鉴权直接通过,否则拦截抛出
TOKEN_INVALID
错误。/* auth.guard.ts */
import {
Injectable,
CanActivate,
ExecutionContext,
HttpException,
HttpStatus,
} from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { Request } from 'express'
import { CommonService } from '@/modules/common/common.service'
@Injectable()
export class UserGuard implements CanActivate {
private readonly reflector: Reflector = new Reflector()
private readonly commonService = new CommonService()
async canActivate(context: ExecutionContext): Promise<any> {
const request = context.switchToHttp().getRequest<Request>()
const roles = this.reflector.get<string[]>('roles', context.getHandler())
//有 all标志的说明接口不做鉴权
if (roles && roles.includes('all')) return true
const token = request.headers['x-auth-id-token']
console.log('guard x-auth-id-token received:', token)
if (!token) return this.fail()
const data = await this.commonService.verifyIdToken(token)
if (data === false) this.fail()
return !!data
}
fail() {
throw new HttpException('TOKEN_INVALID', HttpStatus.FORBIDDEN)
}
}
在controller中 给对应路由加上不鉴权标志
/* demo.controller.ts */
import { Inject, Controller, Get, Query, Post, Body, SetMetadata } from '@nestjs/common'
import { DemoService } from './demo.service'
import { CreateDto, UpdateDto } from './demo.dto'
@Controller('/demo')
export class DemoController {
constructor(@Inject(DemoService) private readonly service: DemoService) {}
@Get('/say')
@SetMetadata('roles', ['all'])
async sayGet(@Query() query: UpdateDto) {
console.log(query)
const name = await this.service.getName()
return `hello sayGet ${name}`
}
@Post('/say')
async sayPost(@Body() body: CreateDto) {
console.log(body)
return 'hello sayPost'
}
}
这样接口
/demo/say
get请求时是不鉴权的,其余接口未加roles则统一走鉴权模式。Interceptor
拦截器主要为在函数执行之前/之后拦截额外处理、转换从函数返回的结果、转换从函数抛出的异常等功能,一般用来处理响应数据,下面案例为返回统一格式的响应数据。
/* app.interceptor.ts */
import {
Injectable,
ExecutionContext,
NestInterceptor,
CallHandler,
HttpStatus,
} from '@nestjs/common'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
@Injectable()
export class AppInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const ctx = context.switchToHttp()
const res = ctx.getResponse()
const req = ctx.getRequest()
// 统一调整返回状态码为200
if (['POST', 'PUT', 'DELETE', 'OPTIONS'].includes(req.method)) {
res.status(HttpStatus.OK)
}
const resMapData = map((data: any) => {
let result = { success: true, message: '请求成功', data: null }
if (data && typeof data === 'object' && 'success' in data) {
result = { ...result, ...data }
} else {
result.data = data
}
return result
})
return next.handle().pipe(resMapData)
}
}
// { success:true,message:'请求成功',data:[] }
入口
对应的根 module 和启动文件,以上模块案例中都是在
main.ts
中使用全局注册的方法,也可以局部注册对应某个模块生效,具体用法可参考官方文档。/* app.module.ts */
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common'
import { ProxyMiddleware } from '@/middleware/proxy.middleware'
import { DemoModule } from '@/modules/demo/demo.module'
@Module({
imports: [
DemoModule,
],
controllers: [],
providers: [],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(ProxyMiddleware).forRoutes({ path: 'java/*', method: RequestMethod.ALL })
}
}
/* main.ts */
import { NestFactory } from '@nestjs/core'
import { NestExpressApplication } from '@nestjs/platform-express'
import { AppModule } from './app.module'
import { AppInterceptor } from '@/interceptor/app.Interceptor'
import { HttpExceptionFilter } from '@/filter/http.filter'
import { UserGuard } from '@/guard/auth.guard'
import { ValidationPipe } from '@/pipe/validation.pipe'
import * as cookieParse from 'cookie-parser'
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
bodyParser: true,
})
//app.setGlobalPrefix('', { exclude: [] })
app.useGlobalFilters(new HttpExceptionFilter())
app.useGlobalInterceptors(new AppInterceptor())
app.useGlobalGuards(new UserGuard())
app.useGlobalPipes(new ValidationPipe())
app.enableCors()
app.use(cookieParse())
const port = process.env.PORT || 4001
await app.listen(port)
console.log(`listen on http://localhost:${port}`)
}
bootstrap()