nestjs 入门学习
Nestjs本身只是一个集成框架,它做的事是继承了现有的nodejs库,在此基础上实现typescript类型系统和拓展其他功能。也就是说它并不重复实现express / fastify等已有的实现http服务,路由之类的东西,它直接继承了这类框架的这些功能并拓展,提供了像angular的依赖注入等功能,同时本身也是基于typescript,所以也支持装饰器模式。
nestjs官方提供支持express和fastify这两个nodejs库(也不是不支持其他的node http库,而是要自己做适配器),默认使用express作为底层适配器。
const app = await NestFactory.create<NestExpressApplication>(AppModule);
你如果熟悉fastify也可以在入口指定使用fastify作为底层实现
import { FastifyAdapter } from '@nestjs/platform-fastify';
...
const app = await NestFactory.create(AppModule, new FastifyAdapter());
nest的入口是main,而main里注册了AppModule模块,这个module模块就是它所有路由控制器和服务的入口。
查看app.module.ts文件,这里的Module装饰器里有3个属性imports,controllers,providers
其中controllers即路由的入口,而providers则是services,只有写在providers里的service才允许被controller使用。即写在providers里的services,nest会自动把services注入到controllers里,这就是它的DI依赖注入,而直接import是不被允许的!
而imports则是子路由modules的入口~
所以一个正常的nest路由应该是一个文件夹里包含module/controllers/services/dto/dao等,不推荐按文件类型放相同文件的操作,不然后期维护真的火葬场。而执行流程是从module->controller->service->dao
插一句可以直接使用vscode plugin nestjs files来生成或者直接执行nest cli上的命令一键生成。
Controller
上面说过了路由即是controller,通过Controller装饰器声明路由前缀,class里的路由会自动继承这个前缀,例如:
import { Controller, Get } from '@nestjs/common'
@Controller('/test')
export class TestController {
@Get()
index() {
return 'test route'
}
}
此时访问/test会直接执行Get装饰器下面的方法,然后返回这个return的字符,Get即requets method,像Post, Put, Delete等均从nestjs/common里导入。
同时这些method支持传入路由path,比如:
...
@Get('detail')
getDetail() {
return ...
}
...
此时访问/test/detail就执行这里Get装饰器/detail的方法。
nestjs自动帮我们处理了请求参数,只要请求content-type是json格式的请求载体,nestjs会自动帮我们格式化json。formdata格式默认不支持,需要自己手动配置!!
获取get请求上的参数,通过Query装饰器可以拿到get请求的参数,例如:
@Get('detail')
detail(@Query('id') id: number, @Query('name') name: string) {
return {
data: {
id,
name,
},
};
}
继续请求/test/detail?id=1&name=abc,此时会返回请求参数到data里。
post请求则通过Body装饰器自动拿到请求参数,例如:
interface AddParams {
account: string;
gendar: 0 | 1;
age: number;
}
...
@Put()
create(@Body() data: AddParams) {
console.log(data);
return {
data,
};
}
Nestjs 会自动帮我们处理response相应格式,如果是object会自动转为json,而普通类型则直接返回其类型。当然我们也可以自己接手返回响应,但是如果接手了响应就一定要有响应给响应报文,否则这个接口就会一直挂着直到超时。一般是不推荐接手响应的,如果非得接手的话也不是不行,比如我们这里底层是用的express
那我们就要这么做,同样通过装饰器拿到原始响应对象然后响应。
import { Controller, Get, Post, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';
...
@Post()
create(@Res() res: Response) {
res.status(HttpStatus.CREATED).send();
}
@Get()
findAll(@Res() res: Response) {
res.status(HttpStatus.OK).json([]);
}
请求成功默认状态码是200,我们也可以更改响应状态码,比如命中缓存的时候201或者重定向301之类的,我们需要通过HttpCode装饰器来设置状态码
@Post()
@HttpCode(204)
create() {
return 'xxx';
}
更改响应头我们可以通过Header装饰器来设置,比如:
@Post()
@Header('Cache-Control', 'no-store')
create() {
return 'xxx';
}
重定向到特定的URL,我们可以使用Redirect装饰器来设置
@Get()
@Redirect('https://blog.iquax.cn', 301)
路由参数则是像/detail/[id]这样的请求直接把参数放到了path里,我们可以通过Param装饰器拿到
@Get(':id')
queryDetail(@Param() params: { id: string }) {
return `detail id is ${params.id}`;
}
但是这么做得小心,只要是请求是/test/xx 的都会被匹配到这个请求里,所以要么把这个方法放到class最后面防止优先匹配到这个请求。
Service
关于这个应该没啥要说的了吧,只需要记住不要在controller里直接import service而是在module里注册service 然后在controller里使用service这一个共识点。同时在controller constructor readonly service 只读service的方式哪里不小心触发修改service的操作。
比如:
...
@Controller('test')
export class IndexController {
constructor(private readonly testService: IndexService) {}
...
找gemini总结了下service需要注意的点:
-
类名加上
@Injectable()装饰器(即使不加有时也能跑,但它是 DI 系统的标识)。 -
不要在 Service 里写
req/res相关逻辑。 -
返回 Promise。
-
大胆抛出 Nest 内置异常。
-
在 Module 的
providers里注册,在 Controller 的constructor里消费。
Module
进阶内容对我来说还是有点抽象的,主要是动态模块这块内容我理解起来比较吃力。
基础功能就是注册controller,注册service。Module装饰还有一个属性是exports,
exports即暴露service便于其他controller能够服用service的方法,其他接收复用service的module需要在imports里导入这个service的module。
你可以把 Module 想象成一个公司:
providers是公司内部的员工。imports是公司购买的外部服务(其他公司的产品)。exports就是公司允许其他公司使用的员工或服务。
或者直接在想要暴露service给其他controller使用的module里加一个Global装饰器用于声明暴露给全局
@Global()
@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule {}
exports不仅仅只能共享service,也能共享module(但是不能共享controller!!)
我之前以为imports是用来做子路由的,翻过文档看才知道服务器并不需要嵌套路由,因为浏览器里嵌套路由可能是为了复用layout,但是服务器并不需要。nest每一个module都可以看成是一个孤岛,它是扁平而不是嵌套的!
DynamicModule 动态模块待实践补充!
DTO
关于dto 你可以理解成typescript interface,但是它比interface更有用,interface只是让你在开发时知道需要什么类型,不符合的类型直接报错提醒。它也就是仅仅在开发环境里有用,但是一旦编译构建后会自动剔除interface,此时请求参数传入不符合的值还需要我们自己手动做校验然后再返回给响应报文,非常的繁琐。而DTO则是interface加强版,不仅仅开发时给予提示类型,在编译部署后仍然可以为你自动处理校验,同时配置后自动返回错误信息。以及借助swagger自动生成swagger文档,搭配class-validator更是可以自动处理格式化值类型。
import { IsString, IsInt, MinLength } from 'class-validator';
export class CreatePostDto {
@IsString()
@MinLength(5)
title: string;
@IsInt()
age: number;
}
nest默认并不会去读DTO里的校验装饰器,所以我们要在main里开启ValidationPipe
// main.ts
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 开启全局验证管道
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 自动剔除 DTO 中未定义的属性(安全)
forbidNonWhitelisted: true, // 发现未定义属性时直接报错(严谨)
transform: true, // 自动根据 DTO 类型转换数据(如字符串转数字)
}));
await app.listen(3000);
}
然后设置校验不通过时返回的消息
// create-post.dto.ts
import { IsString, MinLength, IsInt, Max } from 'class-validator';
export class CreatePostDto {
@IsString({ message: '标题必须是字符串' }) // 自定义错误消息
@MinLength(5, { message: '标题至少需要5个字符' })
title: string;
@IsInt()
@Max(100)
age: number;
}
Middleware
中间件
暂停更新 我要去学Prisma了,不然自己像个原始人还在拼接sql语句
异常拦截器
全局异常处理,用于兜底并返回统一的格式。
nestjs已经内置了很多常见的错误,对应日常错误时的处理。只需要抛出错误,nest会自动将其转成响应报文返回给浏览器,非常的智能。
常见的http code状态比如2xx的请求成功,4xx的比如400请求参数不对,401未登陆,403的没权限,404未找到,500的服务器错误等均有对应的错误处理来兜底。
而400的时候直接抛出BadRequestException并传入错误原因,
401 UnauthorizedException
403 ForbiddenException
404 NotFoundException
500 不用管也别用catch接,nest会自己处理。
以上错误均是基于HttpException继承来实现的,我们也能自定义处理来实现专有的错误。
export class ForbiddenException extends HttpException {
constructor() {
super('Forbidden', HttpStatus.FORBIDDEN);
}
}
我们甚至还能实现一个全局的错误异常处理来保证响应的数据格式一致
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
import { Response, Request } 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 res = exception.getResponse();
const status = exception.getStatus();
response.status(status).json({
code: status,
msg: typeof res === 'string' ? res : (res as any).message,
path: request.url,
timestamp: Date.now(),
});
}
}
在app里注册这个全局异常过滤
app.useGlobalFilters(new HttpExceptionFilter()); // 全局异常处理
此时只要触发了异常,浏览器接收的都会是code/timestamp/path/msg 来保证全局统一返回格式。
拦截器
上面注册了异常拦截器来保持异常的时候返回的数据格式一致,那请求正常的时候怎么保证返回的格式也一致呢?虽然nestjs也支持middleware中间件,但是它不像koa洋葱模型先进后出,能被中间件拦截响应。但是它提供了interceptor来拦截响应,在next之后会执行到interceptor里,此时就能在这里统一处理响应格式了。
同样我们注册一个全局拦截器
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
import { Observable, map } from 'rxjs';
import { Request } from 'express';
export class ResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const ctx = context.switchToHttp();
const request = ctx.getRequest<Request>();
return next.handle().pipe(
map((data) => ({
data: data ?? null,
status: 200,
msg: 'success',
path: request.url,
timestamp: Date.now(),
})),
);
}
}
并同样在入口里注册它
app.useGlobalInterceptors(new ResponseInterceptor()); // 响应拦截器 保持输出格式一致
此时我们请求不管是异常还是正常都能保证输出的格式一致了!