NestJS 是一个 NodeJS 的后端服务框架,它与传统的 NodeJS 框架不一样的是采用了控制反转(IOC)和依赖注入(DI)的模式进行开发
全局安装 NestJS
npm i -g @nestjs/cli
第一个 NestJS 程序
nest new management_nest
执行完毕就会看到 src 下有这样一个目录
- app.controller.ts
这里控制层,这里主要是写路由相关代码以及处理前端传来的一些参数(后面文章会介绍如何接收参数)
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
- app.service.ts
这里是业务层,在这里写一些与业务相关的逻辑。比如对数据库的 CRUD 就可以写到这里
import { Injectable } from "@nestjs/common";
@Injectable()
export class AppService {
getHello(): string {
return "Hello World!";
}
}
- app.module.ts
它可以组织应用程序中的许多功能,如控制器、服务以及可以导入其他模块等
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
- main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
main.ts 则是整个程序的入口文件
我们可以执行pnpm run start:dev开启一个可以热更新的 NestJS 服务,浏览器打开http://localhost:3000/便可发送一个 get 请求到app.controller.ts中的 getHello 函数,然后再调用app.service.ts里的 getHello 函数返回Hello World!
这里我们发现app.controller.ts的 appService 并没有实例化就可以直接使用了,其实这里是因为在app.module.ts已经进行了依赖注入(providers)这里已经将其处理好了
装饰器
在上面的代码中我们看到了譬如@Controller(),@get(),@Module()之类的东西,其实这些东西就是装饰器,你可以把它看作一个函数就行了,比如@Controller()它属于一个类装饰器,他会把下面的类当作参数传入然后进行一些处理从而实现处理路由的功能
为了更好的演示,我们新建一个模块,NestJS 给我们提供了一些命令可以创建对应文件,比如
- 生成一个 module (nest g mo) 。
- 生成一个 controller (nest g co) 。
- 生成一个 service (nest g s) 。
你可以执行nest -h 查看这些命令
我们可以执行nest g res user生成一个 user 模块,包括它的module,controller,service,执行之前可以在nest-cli.json中配置
"generateOptions": {
"spec": false
}
让其不生成测试文件,执行命令我们可以选择 REST API 的形式,这样我们 src 下就会出现了 user 模块
我们可以看到user.controller.ts引入了很多装饰器,已经给我们写好了 CRUD 的模板
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
} from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
@Get()
findAll() {
return this.userService.findAll();
}
@Get(':id')
findOne(@Param() id: string) {
return this.userService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.userService.update(+id, updateUserDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.userService.remove(+id);
}
}
像@Post,@Get,@Patch等就是对应的请求方式装饰器,比如你用 POST 请求调用http://localhost:3000/user就会进入@Post()下面的 create()方法,@Body,@Params则是请求参数装饰器,我们可以从中获取到前端传来的参数
拿第一个Post请求举例,发送 post 请求我们可以使用postman,apifox等工具进行测试,这里我使用apifox进行演示
我们可以先打印一下前端@Body()装饰的createUserDto,然后发送一个 Post 请求
@Post()
create(@Body() createUserDto: CreateUserDto) {
console.log(createUserDto);
return this.userService.create(createUserDto);
}
此时我们就会发现控制台打印了前端传来的 Body 参数
如果想使用别的请求路径,可以在@Post 传入路径,比如@Post('list'),请求路径就会变成/user/list
如果你想获取 Get 请求传来的参数可以使用@Query,获取 Header 中的参数可以使用@Header 等等,这些装饰器有很多这里就不过多介绍了,后面的教程中遇到会作一个详细说明
连接 MySql 数据库
作为一个后端框架肯定是离不开数据库的,NestJS 连接数据库其实很简单,可以先安装@nestjs/typeorm和mysql,typeorm可以让对数据库的 sql 操作映射成对象的操作
pnpm install @nestjs/typeorm typeorm mysql -S
然后你需要在本地安装 mysql 数据库,这里可以自行百度~(注意记住你的用户名和密码以及数据库安装位置)
推荐一个 VSCode 数据库可视化插件Database Client,安装完后连接我们的数据库就能进行一个可视化操作
一切准备就绪,我们新建一个数据库名为easyestadmin,然后开始连接 mysql,我们来到app.module.ts中进行数据库的配置,引入TypeOrmModule调用forRoot进行配置
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { UserModule } from "./user/user.module";
import { TypeOrmModule } from "@nestjs/typeorm";
@Module({
imports: [
TypeOrmModule.forRoot({
type: "mysql",
synchronize: true,
autoLoadEntities: true, //自动加载实体
host: "localhost",
port: 3306, // 端口号
username: "root", // 用户名
password: "root", // 密码
database: "management", //数据库名
synchronize: true, //是否自动同步实体文件,生产环境建议关闭
}),
UserModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
当我们将autoLoadEntities设置为 true 的时候,NestJS 会自动加载数据库实体文件xx.entity.ts文件来创建数据表(如果没有的话),比如 user/entities/user.entity.ts,我们简单加一些字段
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity("user")
export class User {
@PrimaryGeneratedColumn("uuid")
id: number; // 标记为主键,值自动生成
@Column({ length: 30 })
username: string; //用户名
@Column()
password: string; //密码
}
启动项目,然后就会发现自动创建了一个 user 表
如果我们想对 user 表进行一些 CRUD 的操作.可以在user.module.ts中导入
import { Module } from "@nestjs/common";
import { UserService } from "./user.service";
import { UserController } from "./user.controller";
import { User } from "./entities/user.entity";
import { TypeOrmModule } from "@nestjs/typeorm";
@Module({
controllers: [UserController],
providers: [UserService],
imports: [TypeOrmModule.forFeature([User])],
})
export class UserModule {}
user.service.ts中引入使用,比如在 create 函数中创建一条数据
import { Injectable } from "@nestjs/common";
import { CreateUserDto } from "./dto/create-user.dto";
import { UpdateUserDto } from "./dto/update-user.dto";
import { User } from "./entities/user.entity";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>
) {}
async create(createUserDto: CreateUserDto) {
return await this.userRepository.save(createUserDto);
}
async findAll() {
return await this.userRepository.find();
}
findOne(id: number) {
return `This action returns a #${id} user`;
}
update(id: number, updateUserDto: UpdateUserDto) {
return `This action updates a #${id} user`;
}
remove(id: number) {
return `This action removes a #${id} user`;
}
}
再调用 user 接口,传入 username 和 password
我们就完成了一条数据的插入,在 findAll 中查询所有数据,调用 get 请求便可拿到 user 表中的数据
一般来说数据库的配置包含了一些敏感信息不宜写在代码中提交到远程仓库,所以我们可以将配置写在配置文件中,然后提交 git 时候将生产环境的配置文件其忽略,这里我们新建
.env和.env.prod两个文件分别存放开发与生产环境配置
我们还安装 cross-env 来判断我们是处于什么环境
pnpm install cross-env
然后修改package.json中的script
"start:prod": "cross-env NODE_ENV=production node dist/main",
这样我们生成环境的 NODE_ENV 就是 production 了,我们可以根据这个加载不同配置文件。想要加载配置文件,NestJS 给我们提供了@nestjs/config,这个需要手动安装,安装完成之后,在app.module.ts进行配置
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { UserModule } from "./user/user.module";
import { TypeOrmModule } from "@nestjs/typeorm";
import { ConfigModule } from "@nestjs/config";
import * as path from "path";
const isProd = process.env.NODE_ENV == "production";
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: [isProd ? path.resolve(".env.prod") : path.resolve(".env")],
}),
TypeOrmModule.forRoot({
type: "mysql",
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT), // 端口号
username: process.env.DB_USER, // 用户名
password: process.env.DB_PASSWD, // 密码
autoLoadEntities: true, //自动加载实体
synchronize: !isProd, //是否自动同步实体文件,生产环境建议关闭
database: process.env.DB_DATABASE, //数据库名
}),
UserModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
统一的异常处理器
有时候前端会进行一些错误的请求,这时候我们需要返回给他一个异常告知他的请求有问题,我们可以使用 NestJS 内置的异常处理HttpException比如
throw new HttpException('您无权登录', HttpStatus.FORBIDDEN);
客户端就会收到
{
"statusCode": 403,
"message": "您无权登录"
}
但是这样不够灵活,所以我们可以新建一个异常过滤器进行自定义的操作
nest g filter common/filter/http-exception
然后修改common/filter/http-exception/http-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
} from '@nestjs/common';
import { Request, Response } 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 status = exception.getStatus();
response.status(status).json({
code: status,
timestamp: new Date().toISOString(),
path: request.url,
describe: exception.message,
});
}
}
最后在main.ts中进行注册
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { HttpExceptionFilter } from "./common/filter/http-exception/http-exception.filter";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
//看这里看这里看这里~
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
在user.service.ts的 findAll 测试一下
async findAll() {
throw new HttpException('禁止访问', HttpStatus.FORBIDDEN);
return await this.userRepository.find();
}
前端就会收到错误信息的返回
返回格式化拦截器
我们还需要一个返回格式的拦截器对请求成功(状态码为 2xx)的数据进行一个格式化,比如返回这样的格式
{
data:业务参数,
code:状态码,
describe:状态描述
...
}
同样的先执行
nest g interceptor common/interceptor/transform
创建一个拦截器,按照官网示例给的复制过来
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
export interface Response<T> {
data: T;
}
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler
): Observable<Response<T>> {
return next
.handle()
.pipe(map((data) => ({ code: 200, data, describe: "请求成功" })));
}
}
和过滤器一样在main.ts中注册
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { HttpExceptionFilter } from "./common/filter/http-exception/http-exception.filter";
import { TransformInterceptor } from "./common/interceptor/transform/transform.interceptor";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
//看这里看这里看这里~
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new TransformInterceptor());
await app.listen(3000);
}
bootstrap();
我们再将findAll函数改为请求成功的状态
async findAll() {
return await this.userRepository.find();
}
进行请求就会发现数据已经被格式化了
但是这样做之后我们会发现请求成功的 code 只能是 200,一般项目中请求成功还需要很多业务异常状态码返回给前端,所以我们需要新建一个抛出业务异常的类
ApiException我们先创建common/enums/api-error-code.enum.ts用于存放我们的业务状态码,这里简单写几个
export enum ApiErrorCode {
SUCCESS = 200, // 成功
USER_ID_INVALID = 10001, // 用户id无效
USER_NOTEXIST = 10002, // 用户id无效
}
然后在http-exception中新建api.exception.ts,创建一个ApiException类继承HttpException,接受三个参数错误信息,错误码code,http状态码(默认是200)
import { HttpException, HttpStatus } from '@nestjs/common';
import { ApiErrorCode } from '../../enums/api-error-code.enum';
export class ApiException extends HttpException {
private errorMessage: string;
private errorCode: ApiErrorCode;
constructor(
errorMessage: string,
errorCode: ApiErrorCode,
statusCode: HttpStatus = HttpStatus.OK,
) {
super(errorMessage, statusCode);
this.errorMessage = errorMessage;
this.errorCode = errorCode;
}
getErrorCode(): ApiErrorCode {
return this.errorCode;
}
getErrorMessage(): string {
return this.errorMessage;
}
}
然后修改http-exception.filter.ts,可以判断exception是否在ApiException原型链上来确定是调用的是ApiException还是HttpException
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { ApiException } from './api.exception';
@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 status = exception.getStatus();
if (exception instanceof ApiException) {
response.status(status).json({
code: exception.getErrorCode(),
timestamp: new Date().toISOString(),
path: request.url,
describe: exception.getErrorMessage(),
});
return;
}
response.status(status).json({
code: status,
timestamp: new Date().toISOString(),
path: request.url,
describe: exception.message,
});
}
}
然后在 findAll 函数中抛出一个业务异常
async findAll() {
throw new ApiException('用户不存在', ApiErrorCode.USER_NOTEXIST);
return await this.userRepository.find();
}
然后进行请求
此时你会发现请求是成功的,但是 code 值是异常的,符合我们的预期
到这里NestJS的基本配置已经做完了,后续便可直接开始我们业务的开发了,欢迎点赞收藏加关注!