前端也想写后端(1)初识 Nest.js

什么是 Nest.js

Nest.js 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的渐进式框架。它使用 TypeScript 构建并集成了 Express(或 Fastify)以提供强大的功能和灵活性。

Nest.js 的设计目标是使开发人员能够轻松地构建可维护和可扩展的应用程序。它提供了许多内置功能和模块,如依赖注入、中间件、管道、守卫、拦截器、装饰器等,以帮助开发人员更好地组织和管理代码。

Nest.js 官网

开始

我们在创建项目之前,需要安装 nest 的 cli

bash 复制代码
npm i -g @nestjs/cli

然后使用 nest 的 cli 命令创建一个项目

bash 复制代码
nest new [projectName]

我们打开 src,我们会看到我们的入口文件 main.ts

ts 复制代码
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

定义了一个 bootstrap 函数,这个函数使用 NestFactory 创建了一个 app 实例,并监听了 3000 端口,这块我们可以去设置一些全局相关的配置,后续我们会仔细的讲解。

然后我们看到了跟 app 相关的文件, app.module、app.controller、app.service,在 NestJS 中,Module(模块)Controller(控制器) 和 Service(服务) 是核心概念,它们共同构成了应用的基础架构

Module(模块)

模块是 NestJS 应用程序的基本构建块。模块包含控制器、服务和其他模块,并使用依赖注入来管理它们之间的依赖关系。

特点:

  • 每一个应用都至少有一个模块,即根模块,作为应用的入口
  • 模块通过 @Module() 装饰器进行定义,包含了 controllers(控制器数组)、providers(服务等提供者)、imports(导入其他模块)、exports(对外暴露的提供者)四个核心属性。
  • 实现代码的模块化拆分,提高代码的可维护性和可扩展性。
ts 复制代码
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";

@Module({
  imports: [], // 导入其他模块
  controllers: [AppController], // 该模块包含的控制器
  providers: [AppService], // 该模块包含的服务
  exports: [], // 对外暴露,其他模块可以导入
})
export class AppModule {}

Controller(控制器)

控制器负责处理客户端请求,并返回响应。负责接收客户端的请求,并调用适当的服务来处理这些请求。

特点:

  • 控制器通过 @Controller() 装饰器进行定义,一般指定一个路径前缀。
  • 包含了 @Get()@Post()@Put()@Delete() 等路由装饰器,定义请求处理逻辑,对应于不同的 HTTP 请求方法。
  • 一般仅负责接收请求,调用服务层处理业务逻辑,并返回响应。
ts 复制代码
import { Controller, Get } from "@nestjs/common";
import { AppService } from "./app.service";

@Controller() // 可以定义该控制器对应的路径前缀,比如 @Controller('user') 对应就是 /user
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get() // 路由装饰器,定义了该路由的请求方法为 get
  getHello(): string {
    return this.appService.getHello(); // 调用服务层处理业务逻辑
  }
}

Service(服务)

服务是业务逻辑的主要载体,负责处理具体的业务逻辑,比如处理一些数据的操作、复杂的计算等核心的功能。

特点:

  • 服务通过 @Injectable() 装饰器进行定义,表示该类是一个服务。
  • 服务一般包含具体的业务逻辑,如数据库操作、文件操作、第三方的 API 调用等。
  • 服务可以通过依赖注入来使用其他服务,实现代码的解耦和复用。
ts 复制代码
import { Injectable } from "@nestjs/common";

@Injectable()
export class AppService {
  getHello(): string {
    return "Hello World!";
  }

  findAll() {
    return ["a", "b", "c"]; // 模拟查询所有的数据
  }

  create() {
    return "create"; // 模拟创建数据
  }

  findOne(id: number) {
    return `This action returns a #${id} cat`; // 模拟查询一条数据
  }

  update(id: number) {
    return `This action updates a #${id} cat`; // 模拟更新一条数据
  }

  remove(id: number) {
    return `This action removes a #${id} cat`; // 模拟删除一条数据
  }
}

@nestjs/cli 提供了一些命令,比如 nest g controller xxxnest g service xxxnest g module xxx,可以快速生成对应的文件。一般情况下我们会直接快速生成一个功能的模块的代码资源,我们可以使用 nest g resource xxx,它会自动生成对应的 controller、service、module、dto、entity、repository 等文件,并且会自动注入到对应的模块中。

bash 复制代码
nest g resource user

我们选择常用的 REST API 风格,并且可以让他生成 CURD 的代码,我们选择 y,然后我们就可以看到生成了对应的文件,并且自动注入到了对应的模块中。

我们也注意到了,里面有两个文件夹,一个是 dto,一个是 entities,这两个文件夹分别存放了数据传输对象和实体对象,这两个对象是用于数据传输的,我们会在后续的章节中详细讲解。

我们使用命令 npm run start:dev 启动项目,然后我们在浏览器中访问 http://localhost:3000,发现出现了Hello World!,这是因为默认浏览器直接访问http://localhost:3000/,相当于 get 请求 /,就是根路劲,然后被 AppControllergetHello 方法处理,返回了 Hello World!

我们这时候访问http://localhost:3000/user,界面显示 'This action returns all user',这是因为我们访问了 /user,你打开我们刚才通过命令生成的 user 模块,打开 user.controller,@Controller('user')使我们定义了该控制器对应的路径前缀,所以访问 /user,就会进入我们 user 模块的 controller 中,被 UserControllerfindAll 方法处理,返回了 'This action returns all user'。

DTO

DTO 指的是数据传输对象(Data Transfer Object)是一种用于用于定义数据传输格式的对象。主要作用是规范客户端与服务器端之间的数据交互格式,确保数据的合法性和一致性。

DTO 的主要作用是:

  • 数据格式校验: 定义数据传输时的字段类型、必填项、长度限制等规则,配合 NestJS 的验证管道(如 class-validator),可自动校验输入数据的合法性,减少手动校验代码。
  • 明确接口契约: 定义接口的输入输出数据格式,使得客户端和服务端之间的数据交互更加明确,便于团队协作和 API 文档生成,比如 swagger 自动生成文档。
  • 数据封装与隔离: 隔离业务逻辑层与数据传输层,避免直接使用数据库实体传输数据,保护敏感字段(如密码、手机号、银行卡号、身份证号等相关敏感信息)。

在 NestJS 中,DTO 通常使用 class-validator 库进行数据校验,使用 class-transformer 库进行数据转换。

DTO 的定义方式:

  • 使用 class-validator 库进行数据校验,如 @IsString()、@IsInt()、@IsEmail() 等。
  • 使用 class-transformer 库进行数据转换,如 Expose()、Exclude() 等。

安装

bash 复制代码
npm install --save class-validator class-transformer

使用

src/users/dto/create-user.dto.ts

ts 复制代码
import {
  IsString,
  IsEmail,
  MinLength,
  MaxLength,
  Matches,
} from "class-validator";

export class CreateUserDto {
  // 姓名:必须是字符串,长度 2-20
  @IsNotEmpty({ message: "姓名不能为空" })
  @IsString({ message: "姓名必须是字符串" })
  @MinLength(2, { message: "姓名至少 2 个字符" })
  @MaxLength(20, { message: "姓名最多 20 个字符" })
  readonly name: string;

  // 邮箱:必须符合邮箱格式
  @IsNotEmpty({ message: "邮箱地址不能为空" })
  @IsEmail({}, { message: "请输入合法的邮箱地址" })
  readonly email: string;

  // 密码:至少 6 位,包含字母和数字
  @IsNotEmpty({ message: "密码不能为空" })
  @IsString({ message: "密码必须是字符串" })
  @MinLength(6, { message: "密码至少 6 位" })
  @Matches(/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]+$/, {
    message: "密码必须包含字母和数字",
  })
  readonly password: string;
}

我们在 controller 中的创建 create 方法中使用,来校验我们客户端上送的数据

ts 复制代码
import { Controller, Post, Body } from "@nestjs/common";
import { UserService } from "./user.service";
import { CreateUserDto } from "./dto/create-user.dto";

@Controller("user")
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    // 使用 @Body() 装饰器,将请求体中的数据绑定到 createUserDto 对象上
    return this.userService.create(createUserDto);
  }
}

然后我们需要启用我们的校验,在 main.ts 中添加

ts 复制代码
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

这时候我们通过 postman 来测试一下,我们给 body 什么都不写,先来试一下

发现确实拦截住了,并且返回了我们定义的错误信息,但是似乎所有的规则都校验了,其实我们的验证管道有很多配置,比如:

ts 复制代码
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true, // 自动移除未在 DTO 中定义的字段
      forbidNonWhitelisted: true, // 对未定义的字段抛出错误
      transform: true, // 自动将请求数据转换为 DTO 实例
      stopAtFirstError: true, // 在第一个验证错误时停止验证
    })
  );
  await app.listen(process.env.PORT ?? 3000);
}

当我们开启了 stopAtFirstError 的时候就只会返回第一个错误,我们再试一下

我们发现其实他依旧是每个字段都校验,只是返回了每个字段第一个错误,有些时候我们希望只返回一个错误,那我们有什么办法吗?有的,我们可以通过自定义异常过滤器来实现,这样我们就可以自定义返回的错误信息了,我们会在后面处理正常报文返回以及错误返回格式的时候详细讲解。

本节先到这,我们的教程依旧会持续更新,大家持续关注。

本专栏源码地址

相关推荐
用户21411832636021 小时前
零成本搭建 AI 应用!Hugging Face 免费 CPU 资源实战指南
后端
澡点睡觉1 小时前
golang的包和闭包
开发语言·后端·golang
涡能增压发动积4 小时前
Browser-Use Agent使用初体验
人工智能·后端·python
探索java5 小时前
Spring lookup-method实现原理深度解析
java·后端·spring
lxsy5 小时前
spring-ai-alibaba 之 graph 槽点
java·后端·spring·吐槽·ai-alibaba
码事漫谈5 小时前
深入解析线程同步中WaitForSingleObject的超时问题
后端
码事漫谈5 小时前
C++多线程同步:深入理解互斥量与事件机制
后端
少女孤岛鹿5 小时前
微服务注册中心详解:Eureka vs Nacos,原理与实践 | 一站式掌握服务注册、发现与负载均衡
后端
CodeSaku5 小时前
是设计模式,我们有救了!!!(四、原型模式)
后端