NestJS 学习笔记
简介
Nest 是一个用于构建 Node.js 服务器端应用程序的框架。内置 TypeScript(也允许用纯 JavaScript 编写代码)并结合了 OOP(面向对象编程),FP(函数式编程)和 FRP(函数式响应编程)的元素及思想,基于装饰器的语言特性而创建,设计灵感来自于 Angular。
Angular 的很多模式又来自于 Java 中的 Spring 框架,依赖注入、面向切面编程等,所以你可以认为: Nest 是 Node.js 版的 Spring 框架。
同类型框架比较
- Midway
- Egg
- Express
起步
相关命令
#安装脚手架
$ npm i -g @nestjs/cli
#使用脚手架创建一个项目
$ nest new [project-name]
#默认启动项目命令
$ npm run start
#打包项目
$ nest run build
脚手架初始化项目 - 选项
- Which package manager would you to use?(选择你想使用的包管理器)
yarn | npm(默认) | pnpm
初始目录结构
src
├── app.controller.spec.ts 对于基本控制器的单元测试样例
├── app.controller.ts 带有单个路由的基本控制器示例
├── app.module.ts 应用程序的根模块
├── app.service.ts 带有单个方法的基本服务提供者
└── main.ts 应用程序入口文件
其他指令
#查看帮助选项
$ nest --help|-h
#自动在目录中创建一个名为[module-name]的控制器模块文件
$ nest generate|g co [module-name]
#显示项目信息
$ nest info
#更新依赖包
$ nest update|u
#添加依赖包
$ nest add [library-name]
Generate命令说明(点击查看):
名称 | 别名 | 说明 |
---|---|---|
application | application | 在当前目录创建新的应用(同 new 指令) |
class | cl | 生成一个空 class 文件 |
configuration | config | 生成 CLI 配置文件 |
controller | co | 生成并声明一个控制器模块 |
decorator | d | 生成一个自定义装饰器文件 |
filter | f | 生成并声明一个过滤器模块 |
gateway | ga | 生成并声明一个网关模块 |
guard | gu | 生成并声明一个权限守卫模块 |
interceptor | in | 生成并声明一个拦截器模块 |
interface | interface | 生成并声明一个接口定义模块 |
middleware | mi | 生成并声明一个中间件模块 |
module | mo | 生成并声明一个模块管理文件 |
pipe | pi | 生成并声明一个管道模提供者 |
resolver | r | 生成并声明一个 GraphQL 解析器模块 |
service | s | 生成并声明一个服务模块 |
library | lib | 在 monorepo 中生成新库 |
sub-app | app | 在 monorepo 中生成新应用程序 |
resource | res | 生成一个完整的 CRUD 资源目录 |
定义
Controller(控制器)
通俗来说就是路由 Router,负责处理客户端传入的请求参数并向客户端返回响应数据,也可以理解是 HTTP 请求的逻辑处理。
注:要使用 CLI 创建控制器类,只需执行 $ nest g co [name] 命令。
示例:
/* dto/create-cat.dto.ts */
//定义一个Dto类,规定请求的入参格式。
export class CreateCatDto {
readonly name: string;
readonly age: number;
readonly breed: string;
}
------------------------------------
/* cats.controller.ts */
import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common';
import { CreateCatDto, UpdateCatDto, ListAllEntities } from './dto';
@Controller('cats') //定义了一个前缀为'cats'的路由模块
export class CatsController {
@Post() //通过Post方法访问到"/cats/"路由
create(@Body() createCatDto: CreateCatDto) {
return '此路由将添加一条新数据';
}
@Get() //通过Get方法访问到"/cats/"路由
findAll(@Query() query: ListAllEntities) {
return `此路由将返回所有数据 (limit: ${query.limit} items)`;
}
@Get(':id') //通过Get方法访问到"/cats/${id}"路由
findOne(@Param('id') id: string) {
return `此路由将根据${id}返回指定数据`;
}
@Put(':id') //通过Put方法访问到"/cats/${id}"路由
update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
return `此路由将根据${id}更新指定数据`;
}
@Delete(':id') //通过Delete方法访问到"/cats/${id}"路由
remove(@Param('id') id: string) { //delete为js保留关键字,可以使用remove命名
return `此路由将根据${id}删除指定条目`;
}
}
Providers(提供者)
Providers 是一个用@Injectable()装饰器注释的类。许多基本的 Nest 类可能被视为 provider,如 service, repository, factory, helper 等,通过 constructor(构造器) 注入依赖关系。
注:要使用 CLI 创建服务类,只需执行 $ nest g s [name] 命令。
最常见的是使用@Injectable()装饰器创建一个提供数据操作服务的类,即 service 模块。
示例:
/* interfaces/cat.interface.ts */
//声明一个接口,规定请求的入参格式。
export interface Cat {
name: string;
age: number;
breed: string;
}
------------------------------
/* cats.service.ts */
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;
}
}
然后在 CatsController 里使用它:
/* dto/create-cat.dto.ts */
export class CreateCatDto {
readonly name: string;
readonly age: number;
readonly breed: string;
}
----------------------------------
/* cats.controller.ts */
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 {
constructor(private catsService: CatsService) {} //在这里通过构造器注入服务模块
@Post()
async create(@Body() createCatDto: CreateCatDto) {
//Nest已经自动把构造器中注入的依赖转换为实例,这里就可以直接通过this调用了。
this.catsService.create(createCatDto);
}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
}
定义了服务模块(提供者)和控制器路由模块(使用者)之后,还需要在模块管理文件中进行注册。
示例:
//app.module.ts
import { Module } from "@nestjs/common";
import { CatsController } from "./cats/cats.controller";
import { CatsService } from "./cats/cats.service";
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class AppModule {}
然后我们就拥有了现在的目录结构:
src
├── cats
│ ├──dto
│ │ └──create-cat.dto.ts
│ ├── interfaces
│ │ └──cat.interface.ts
│ ├──cats.service.ts
│ └──cats.controller.ts
├──app.module.ts
└──main.ts
Module(模块)
模块是具有 @Module() 装饰器的类。 @Module() 装饰器提供了元数据,Nest 用它来组织应用程序结构。
要使用 CLI 创建模块,只需执行 $ nest g module cats 命令。
每个 Nest 应用程序至少有一个模块,即根模块(app.module)。一般情况下应用程序可以按照功能划分若干个子模块,比如 cats 模块。
示例:
/* 子模块 cats/cats.module.ts */
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService] //若需要在其它模块中CatsService实例,就需要把它导出去。
})
//Provider也可以注入到模块的导出类中,方便用于其他配置。
export class CatsModule {
constructor(private readonly catsService: CatsService) {}
/// 可以通过注入Provider,在这里做一些其它事情...
}
------------------------------------------
/* 根模块 app.module.ts */
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule], //在这里导入子模块
})
export class ApplicationModule {}
按照上述的导出模块以共享实例,可能不能够满足某些特殊情况。比如我们在很多地方中都用到了相同的模块,或者想要一些模块即取即用(比如 helper,数据库连接等),如果在每个地方都这样导入导出就太麻烦了。所以可以把这类模块,通过 @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 {}
通过按照功能划分模块之后,我们的项目结构现在是这样的:
src
├──cats
│ ├──dto
│ │ └──create-cat.dto.ts
│ ├──interfaces
│ │ └──cat.interface.ts
│ ├─cats.service.ts
│ ├─cats.controller.ts
│ └─cats.module.ts
├──app.module.ts
└──main.ts
Middleware(中间件)
中间件是在路由处理程序(controller)之前调用的函数,使用@Injectable()装饰器定义类。提供三个回调参数,请求对象(request)、响应对象(response)和中间件函数(next())。
注:要使用 CLI 创建中间件函数,只需执行 $ nest g mi [name] 命令。
中间件功能:
- 执行任何代码。
- 对请求和响应对象进行更改。
- 结束请求-响应周期。
- 调用堆栈中的下一个中间件函数。
- 如果当前的中间件函数没有结束请求-响应周期, 它必须调用 next() 将控制传递给下一个中间件函数。否则, 请求将被挂起。
示例:
/* logger.middleware.ts */
import { Injectable, NestMiddleware } from "@nestjs/common";
import { Request, Response, NextFunction } from "express";
import { CatsService } from "./cats/cats.service";
//使用类的方式创建中间件
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
//同样支持依赖注入
constructor(private readonly catsService: CatsService) {}
use(req: Request, res: Response, next: NextFunction) {
console.log(this.catsService.findAll());
next();
}
}
//使用函数的方式创建中间件
export function logger(req, res, next) {
console.log(`Request...`);
next();
}
然后在 app.module 中挂载(中间件并不能在@Module()装饰器中像 controller 数组一样列出,必须在实现 NestModule 接口的类上使用 configure 方法来设置和挂载)。
示例:
/* app.module.ts */
import { Module, NestModule, MiddlewareConsumer } from "@nestjs/common";
import {
LoggerMiddleware,
logger,
} 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, logger) //可以指定一个或多个中间件
.forRoutes("cats"); //通过forRoutes可以将中间件限制在制定路由下面使用。
}
}
MiddlewareConsumer 是专门用来管理中间件的帮助类。forRoutes 方法支持传入字符串、对象或控制器类,并支持正则表达式的匹配。并且可以通过前置方法 exclude 来标记需要排除的路由。
示例:
consumer
.apply(LoggerMiddleware)
.exclude(
{ path: "cats", method: RequestMethod.GET },
{ path: "cats", method: RequestMethod.POST },
"cats/(.*)"
)
.forRoutes(CatsController);
// forRoutes({ path: 'cats', method: RequestMethod.GET })
// forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });
也可以使用入口文件(main)的 app.use()方法,全局挂载中间件到所有路由上:
const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);
Filter(过滤器)
过滤器其实是 Nest 提供的内置方法类,例如 HttpException(基础异常类),当我们想在控制器中抛出异常时,就可以使用 new HttpException 方法来提示客户端发生错误。
注:要使用 CLI 创建中间件函数,只需执行 $ nest g f [name] 命令。
示例:
/* cats.controller.ts */
@Get()
async findAll() {
throw new HttpException({
status: HttpStatus.FORBIDDEN,
error: '这里可以自定义错误信息',
}, HttpStatus.FORBIDDEN);
}
------------------------------------
/* 当客户端访问上述路由方法时,HTTP请求就会响应如下错误 */
{
"status": 403,
"error": "这里可以自定义错误信息"
}
我们可以把通用的基础异常信息封装到一个类(从HttpException继承)里,这样就不需要每次在调用的时候传值了。
示例:
/* forbidden.exception.ts */
export class ForbiddenException extends HttpException {
constructor() {
super({
status: HttpStatus.FORBIDDEN,
error: '这里可以自定义错误信息',
}, HttpStatus.FORBIDDEN);
}
}
---------------------------
/* cats.controller.ts */
@Get()
async findAll() {
throw new ForbiddenException();
}
----------------------------
/* 当客户端访问上述路由方法时,HTTP请求就会响应如下错误 */
{
"status": 403,
"error": "这里可以自定义错误信息"
}
Nest内置的继承自HttpException的异常(点击查看):
- BadRequestException
- UnauthorizedException
- NotFoundException
- ForbiddenException
- NotAcceptableException
- RequestTimeoutException
- ConflictException
- GoneException
- PayloadTooLargeException
- UnsupportedMediaTypeException
- UnprocessableException
- InternalServerErrorException
- NotImplementedException
- BadGatewayException
- ServiceUnavailableException
- GatewayTimeoutException
除了上述内置的基础异常类之外,我们还可以自己实现一个ExceptionFilter异常类,以便于自定义返回信息的格式。自定义的异常过滤器负责捕获Catch装饰器定义的异常类所抛出的异常,可以通过express提供的Request和Response对象获取当前的上下文请求响应数据。
示例:
/* http-exception.filter.ts */
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
//使用Catch装饰器定义一个异常类,实现ExceptionFilter接口
//用Catch参数声明要捕获的异常类别,该示例声明捕获基础异常类(HttpException),如不填,则捕获所有应用异常。
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>(); //获取res对象
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
const exceptionRes: any = exception.getResponse(); //获取额外的返回数据
const {
message,
} = exceptionRes;
response
.status(status)
.json({ //自定义异常返回格式
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message,
});
}
}
-----------------------------------
/* cats.controller.ts */
import { Controller, Get, HttpStatus, } from '@nestjs/common';
import { CatsService } from './cats.service';
import { HttpExceptionFilter } from './http-exception.filter';
//HttpExceptionFilter过滤器通过UseFilters装饰器绑定到类或原型法上
@Controller('cats')
@UseFilters(HttpExceptionFilter) //也可以传递实例@UseFilters(new HttpExceptionFilter())
export class CatsController {
constructor(private catsService: CatsService) {}
@Get()
//@UseFilters(HttpExceptionFilter) 也可以绑定到方法上
async findAll() {
throw new HttpException({ //使用基础异常类实例抛出异常,等待捕获。
status: HttpStatus.FORBIDDEN,
message: '这里可以自定义错误信息',
}, HttpStatus.FORBIDDEN);
}
}
还可以把过滤器设置为全局,挂载到入口文件(main)的app上:
/* main.ts */
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
如果把过滤器通过app挂载到全局,则不能在控制器中使用依赖注入。如果想使用依赖注入,我们还可以把过滤器通过useClass绑定在根模块上:
/* app.module.ts */
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_FILTER, //该提供者的类型
useClass: HttpExceptionFilter,
},
],
})
export class AppModule {}
Pipe(管道)
管道应提供"数据转换"或"数据校验"的功能,用@Injectable()装饰器来声明,实现一个PipeTransform接口。
注:要使用 CLI 创建管道,只需执行 $ nest g pi [name] 命令。
Nest提供了8个内置管道(点击查看):
- ValidationPipe
- ParseIntPipe
- ParseBoolPipe
- ParseArrayPipe
- ParseUUIDPipe
- DefaultValuePipe
- ParseEnumPipe
- ParseFloatPipe
示例:
/* CustomPipe.pipe.ts */
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class CustomPipe implements PipeTransform { //实现PipeTransform接口
//必须提供一个transform方法
transform(value: any, metadata: ArgumentMetadata) {
if (!isNaN(parseInt(value)) { //校验通过则直接返回值
/* 在这里可以对原始值做一些处理,然后返回处理后的值。
const val = parseInt(value)
return val
*/
return value;
}else{ //如不通过,则抛出一个异常。
throw new HttpException(`值必须为整型!`, HttpStatus.BAD_REQUEST)
}
}
}
transform方法有两个参数,value(当前要处理的参数),metadata(当前参数的元数据对象)。
metadata包含的属性(点击查看)
参数 | 描述 |
---|---|
type | 说明该参数是一个 body @Body(),query @Query(),param @Param() 还是自定义参数。 |
metatype | 参数的数据类型,例如 String。 如果在函数签名中省略类型声明,或使用原生 JavaScript,则为 undefined。 |
data | 传递给装饰器的字符串,例如 @Body('string')。 如果将括号留空,则为 undefined。 |
/* dto/create-cat.dto.ts */
export class CreateCatDto {
readonly name: string;
readonly age: number;
readonly breed: string;
}
----------------------------------
/* cats.controller.ts */
import { Controller, Post, Body, UsePipes } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CustomPipe } from './CustomPipe.pipe';
@Controller('cats')
//@UsePipes(CustomPipe) //可以装饰类
export class CatsController {
constructor(private catsService: CatsService) {}
@Post()
//@UsePipes(new CustomPipe()) //可以装饰方法
async create(@Body(new CustomPipe()) createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
}
在很多情况下,我们所创建的管道是通用的方法,所以可以把它设置为全局的,使用方法和过滤器一样。
示例:
/* main.ts */
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 可以使用app.useGlobalPipes方法挂载到全局应用上。
app.useGlobalPipes(new CustomPipe());
await app.listen(3000);
}
bootstrap();
--------------------
/* app.module.ts */
//也可以通过模块绑定到全局
@Module({
providers: [
{
provide: APP_PIPE,
useClass: CustomPipe
}
]
})
export class AppModule {}
Guard(路由守卫)
Guard应使用@Injectable()装饰器定义,实现一个CanActivate接口,提供控制器路由的前置防卫功能。使用方式类似于过滤器、管道和拦截器。
注:要使用 CLI 创建Guard,只需执行 $ nest g gu [name] 命令。
示例:
/* auth.guard.ts */
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate { //实现CanActivate接口
// 必须提供一个canActivate方法,返回一个布尔值。
// 返回值 true:允许本次请求,false:忽略本次请求并返回HttpException异常。
canActivate(
context: ExecutionContext, //获取上下文
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest(); //获取当前请求
///可以在这里做一些事情,比如校验Token之类...
return true; //返回true则允许本次请求
}
}
然后在控制器中使用UseGuards装饰器声明。像过滤器一样,可以装饰类也可以装饰方法,也可以设置全局。
示例:
/* dto/create-cat.dto.ts */
export class CreateCatDto {
readonly name: string;
readonly age: number;
readonly breed: string;
}
----------------------------------
/* cats.controller.ts */
import { Controller, Post, Body, UseGuards } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { AuthGuard } from './auth.guard';
@Controller('cats')
// @UseGuards(AuthGuard)
export class CatsController {
constructor(private catsService: CatsService) {}
@Post()
@UseGuards(AuthGuard)
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
}
全局守卫:
/* main.ts */
const app = await NestFactory.create(AppModule);
//使用app.useGlobalGuards方法挂载到全局
app.useGlobalGuards(new RolesGuard());
-------------------------------------
/* app.module.ts */
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
//也可以使用模块绑定到全局
@Module({
providers: [
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class AppModule {}
下面举一个完整的例子,现在有一个路由模块(控制器类),需要按照不同的用户角色来执行不同的逻辑,而分辨角色的逻辑不应该直接写在控制器里,我们可以把它先在路由守卫里分类出来,然后再使请求继续。
示例:
/* 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 roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
//自定义的matchRoles方法,可根据实际情况编写逻辑。
return matchRoles(roles, user.roles);
}
}
----------------------------------
/* roles.decorator.ts */
import { SetMetadata } from '@nestjs/common';
//这里我们为了方便,自定义了一个设置Roles元数据的装饰器
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
----------------------------------
/* cats.controller.ts */
@Post()
@Roles('admin') //也可以直接在这里设置元数据@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
----------------------------------
/* app.module.ts */
@Module({
providers: [
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class AppModule {}
Interceptor(拦截器)
Interceptor应使用@Injectable()装饰器定义,实现一个NestInterceptor接口。使用方式类似于过滤器、管道和路由守卫。
Decorator(自定义装饰器)
自定义装饰器的使用方法其实就是js/ts原生的装饰器使用方式,然后根据Nest提供的上下文对象(ExecutionContext),自己写装饰器的函数内容,区别于直接import导入使用的Nest内置的装饰器。
示例:
/* user.decorator.ts */
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator((data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user && user[data] : user;
});
---------------------------------
/* cats.controller.ts */
import { Controller, Get } from '@nestjs/common';
import { User } from './user.decorator.ts';
@Controller('cats')
export class CatsController {
@Get()
async findOne(@User('firstName') firstName: string) {
console.log(`Hello ${firstName}`);
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」