路由
控制器配置路由
nestjs的路由是通过控制器配置的,控制器层面配置Cgi路径:控制器路由前缀[可选] + 方法路由前缀[可选]
import { Controller, Get, Post } from '@nestjs/common';
import { AppService } from './app.service';
// @Controller() 修改1
@Controller('app')
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
// --- 修改 2---
@Get('name')
getName(): String {
return 'LW';
}
// --- 修改 3---
@Post('age')
getAge(): Number {
return 18;
}
}
如上,将AppController控制器添加路由前缀app(修改1), 添加一个了name路由前缀的get方法(修改2)和age路由前缀的post方法(修改3)。保存等重新编译后刷新浏览器发现localhost:3000提示404,而添加上app前缀(完整路由:localhost:3000/app)才正常显示出Hello World!
修改2的路由(localhost:3000/app/name)
修改3的路由(localhost:3000/app/age)这里借助postman请求post cgi
全局路由配置
nestjs还支持配置全局路由前缀
添加全局路由前缀后,所有的请求都需要添加上前缀。例如此处的localhost:3000/app 就变成了 localhost:3000/newapi/app
控制器方法中一般不写过多业务逻辑,而是将业务逻辑写在
Service中,比如AppController中的getHello就直接调用AppService的getHello方法。更多的路由配置细节请查看中文文档,这里就不过多介绍了。
路由通配符
支持?、+、*正则表达式配置路由
@Get('regexp/note_*')
getReg(): String {
return '通配路由';
}
@Get('blog/:id')
getRegExp(@Param() params): String {
return `文章id: ${params.id}`;
}
静态资源
可以通过useStaticAssets指定静态资源目录,还可以指定虚拟目录。
比如设置public为静态资源目录, 并指定/static为虚拟目录
app.useStaticAssets(join(__dirname, '..', 'public'), {
prefix: '/static',
});
public下有个index.html文件
然后可以通过路由访问了
模块
前面提到nestjs都是按模块划分的,现手动一步步创建一个用户模块。nestjs支持通过命令快速创建模块、控制器、服务器、资源等,后面会介绍,这步可自行选择跳过。这里我为了加深了解选择手动创建,后续还是会通过命令创建。
- 首先在src下新建
user文件夹。 - src/user下创建user.module.ts。前面提到模块是通过module装饰器装饰的类,module装饰器引入自
@nestjs/common,module装饰器提供四个可选参数控制器controllers、提供者providers、需要导入的模块imports、导入的模块exports。 记得这些不用对照自己也能写敲出module文件的基本结构,当然也不用记,后面项目写多了自然就记住了。
import { Module } from '@nestjs/common';
@Module({
controllers: [],
providers: [],
imports: [],
exports: []
})
export class UserModule {}
- 在需要导入的模块中导入该user模块,这里需要在app.module.ts中@module装饰器imports中导入
// app.module.ts
...
import { UserModule } from './user/user.module';
@Module({
imports: [UsersModule],
...
- 创建了模块文件,接下来就需要创建控制器文件了。在src/user下创建user.controller.ts,并在user.module.ts中引入该控制器,内容如下。
//user.module.ts
...
import { UserController } from "./user.controller";
...
controllers: [UserController],
...
// user.controller.ts
import { Controller, Get, Param } from "@nestjs/common";
@Controller('user')
export class UserController {
constructor() {}
@Get(':id')
getUser(@Param() params) {
return {
name: 'lw-' + params.id,
age: 18
}
}
}
- 此时getUser方法就可以正常访问了
- 这里控制器只是简单返回的了个对象,直接在控制器中返回也可以,但实际开发中逻辑功能因写在Service中。同样的在src/user下创建user.service.ts,修改后内容如下。
// user.module.ts
import { Module } from "@nestjs/common";
import { UserController } from "./user.controller";
import { UserService } from "./user.service";
@Module({
controllers: [UserController],
providers: [UserService],
imports: [],
exports: []
})
export class UsersModule {}
// user.controller.ts
import { Controller, Get, Param } from "@nestjs/common";
import { UserService } from "./user.service";
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get(':id')
getUser(@Param() params) {
return this.userService.getUser(params.id);
}
}
// user.service.ts
import { Injectable } from "@nestjs/common";
@Injectable()
export class UserService {
getUser(userId) {
return {
name: 'lw-' + userId,
age: 18
}
}
}
cli快捷指令
使用nest generate|g [options] <schematic> [name] [path]可快速创建模块、控制器等。
这里创建一个Login模块为例。
-
执行
nest g mo login创建一个login模块文件,会自动在src下创建login文件夹,里面包含一个login.module.ts文件,并且在app.module.ts中自动引入LoginModule。 -
执行
nest g co login创建一个login控制器文件,自动在src/login下创建login.controller.ts文件,并自动在src/login/login.module.ts引入LoginController -
执行
nest g s login创建一个login服务文件,自动在src/login下创建login.service.ts文件,并自动在app.module.ts中引入LoginService
简单使用几个指令就能创建一个模块,比手敲代码要方便的多,当然还有更方便的nest g res login就会创建整个Login功能模块(CRUD资源),并包含基础的dto和entity文件。nestjs提供了众多快捷的CLI,请查阅中文文档CLI用法
数据操作
前面介绍了基本的路由配置以及模块的创建,创建好模块后,既然是后端项目自然,接下来就是数据库操作了。
安装数据库(MySQL)
请自行百度😂
安装vscode数据库客户端插件Database Client
如图安装该插件
安装完后点击左侧的数据库图标,然后在右侧输入名称(随意,可不填)、密码(安装数据库时设置的密码),点击连接即可。如连接失败,检查一下数据库服务是否开启,以及账号连接配置是否正确。
使用TypeORM连接MySQL
安装依赖
yarn add @nestjs/typeorm typeorm mysql2
连接数据库
方式1
// app.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';
import { Module } from '@nestjs/common';
...
@Module({
imports: [
...
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'blog',
entities: [],
synchronize: true,
})
],
...
})
export class AppModule {}
上面通过将数据库配置放到TypeOrmModule.forRoot里,还可以将配置放到文件上。
方式2
在src同级目录下创建ormconfig.json文件
//ormconfig.json
{
"type": "mysql",
"host": "localhost",
"port": 3306,
"username": "root",
"password": "123456",
"database": "blog",
"entities": ["dist/**/*.entity{.ts,.js}"],
"synchronize": true
}
然后app.module.ts文件中,TypeOrmModule.forRoot就可以不带参数了。
// app.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';
import { Module } from '@nestjs/common';
...
@Module({
imports: [
...
TypeOrmModule.forRoot()
],
...
})
export class AppModule {}
注意,
ormconfig.json文件由typeorm库载入,因此,任何上述参数之外的属性都不会被应用(例如由forRoot()方法内部支持的属性–例如autoLoadEntities和retryDelay())
CURD
通过TypeORM可以将实体映射到数据库表,就不用手动执行建库操作。下面简单的创建一个实体来熟悉下大致流程。
创建src/user/entities/user.entity.ts文件。
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity('users')
export class UsersEntity {
// 主键
@PrimaryGeneratedColumn()
id: number;
// 字符串,对应varchar(20)
@Column({ length: 20 })
username: string;
// 字符串,没有长度限制,默认varchar(255)
@Column()
nickname: string;
// 同上,设置select: false, 查询的时候就不会返回。
// 实际开发过程中密码应加密
@Column({ select: false })
password: string;
// 数字,对应int
@Column()
age: number;
}
这个实体文件就对于一个表的描述,并且数据库配置中开启了synchronize: true(生产环境建议关闭),连接数据库后,就会根据该实体文件自动创建users表。
开发中遇到过个问题,数据库为mysql,在开启
synchronize后,提示数据表已存在的错误,后来发现实体定义的命名不对,需要小写(例如@Entity('小写'))。
// user.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersEntity } from './entities/user.entity';
//...
@Module({
//...
imports: [TypeOrmModule.forFeature([UsersEntity])],
})
在需要使用该实体的模块中通过TypeOrmModule.forFeature导入该实体。
// app.module.ts
import { UsersEntity } from './user/entities/user.entity';
// ...
@Module({
imports: [
TypeOrmModule.forRoot({
// ...
entities: [UsersEntity],
})
],
// ...
})
另外还需AppModule数据库连接选项加入该实体。而且后面每加一个实体,都需要在这里导入。当然,可以开启autoLoadEntities自动加载实体,这样每个通过forFeature方法注册的实体都将自动添加到配置对象的entities数组中。
// app.module.ts
//import { UsersEntity } from './user/entities/user.entity';
// ...
@Module({
imports: [
//...
TypeOrmModule.forRoot({
// ...
//entities: [UsersEntity],
autoLoadEntities: true, // 开启自动加载实体
})
],
// ...
})
然后后查看数据库会发现已经创建一个Users表
再也不用像以前需要手敲sql语句了🤓,接下来试试插入和查询操作。调整如下
// user.controller.ts
import { Body, Controller, Get, Param, Post } from "@nestjs/common";
import { UserService } from "./user.service";
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get(':username')
getUser(@Param() params) {
return this.userService.getUser(params.username);
}
@Post('create')
createUser(@Body() user) {
return this.userService.createUser(user);
}
}
// ------------------------
// user.service.ts
import { HttpException, Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { UsersEntity } from "./entities/user.entity";
@Injectable()
export class UserService {
constructor(
@InjectRepository(UsersEntity)
private readonly usersRepository: Repository<UsersEntity>
) { }
async getUser(username: string): Promise<UsersEntity> {
let user = await this.usersRepository.findOne({ where: { username } });
return user;
}
async createUser(user: Partial<UsersEntity>): Promise<UsersEntity> {
console.log(user);
let { username } = user;
if(!username) {
throw new HttpException(`${username}不能为空!`,401)
}
let usr = await this.usersRepository.findOne({ where: { username } });
if(usr) {
throw new HttpException(`${username}已存在!`,401);
}
return await this.usersRepository.save(user);
}
}
接着使用postman来试试接口
-
创建
成功,插入了一条数据
-
查询
也毛得问题,成功返回了大帅哥的信息。
这只是简单示例了一下数据的操作,实际数据库操作要比这复杂的多(比如多表连表查询,几千行sql语句你值得拥有[虽说没遇到过,之前公司老系统手敲sql很容易就上千行了])、规范(比如密码字符长度、密码加密等)等
数据库配置优化
前面例子把数据库的连接信息都写到app.module.ts中了,而且没有区分开发和生产的配置,改造一下。
- 在package.json中scripts中加入变量标识
先安装
cross-env打平环境变量设置差异
yarn add cross-env -D
// package.json
...
"scripts": {
"start": "nest start", // 加入NODE_ENV环境变量
"start:dev": "cross-env NODE_ENV=dev nest start --watch", // 加入NODE_ENV环境变量
}
- 在src同级创建
.env.dev(开发配置)和.env.prod(生产配置)
// .env.dev
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=123456
DB_DATABASE=blog
!------------------
// .env.prod
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=123456
DB_DATABASE=blog
- 创建
src/config/db.config.ts
// src/config/db.config.ts
import * as fs from 'fs';
import * as path from 'path';
// 这里只简单区分开发和生产,不是开发就当作生产
export const isProd = process.env.NODE_ENV !== 'dev';
function getEnvFilePath() {
const localEnv = path.resolve('.env.dev');
const prodEnv = path.resolve('.env.prod');
if (!fs.existsSync(localEnv) && !fs.existsSync(prodEnv)) {
throw new Error(`缺少配置文件 ${isProd ? prodEnv : localEnv}`);
}
const filePath = isProd && fs.existsSync(prodEnv) ? prodEnv : localEnv;
return filePath;
}
export const dbEnvFilePath = getEnvFilePath()
- 使用
@nestjs/config(需先安装yarn add @nestjs/config)提供的配置模块功能,将app.module.ts做如下修改
// app.module.ts
import { ConfigModule, ConfigService } from '@nestjs/config';
import { dbEnvFilePath, isProd } from './config/db.config';
...
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: [dbEnvFilePath]
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
type: 'mysql',
autoLoadEntities: true,
synchronize: !isProd,
host: configService.get('DB_HOST'),
port: configService.get<number>('DB_PORT'),
username: configService.get('DB_USER'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_DATABASE'),
})
}),
],
})
export class AppModule {}
这样便对数据库的配置做了个最简单的优化
异常过滤器
虽然可以正常curd了,但请求还是脆弱粗糙。当我们的请求缺少参数将会发生!
报500错误了,这个是因为请求参数缺少导致在保存到数据库时出现了异常后被nestjs处理的结果。和前面手动抛出的异常(throw new HttpException(`用户${username}不存在`,401);)类似,这些不管是手动抛出的异常还是程序异常,整个应用程序中的所有抛出的异常都将由
Nestjs内置的异常层处理。
自定义(异常)过滤器
- 创建一个捕获Http异常的过滤器src/common/filter/
http-exception.filter.ts,编码如下
// common/filter/http-exception.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
// 这里扑获Http异常,不注入参数将扑获所有异常
@Catch(HttpException)
export class HttpExceptionFilter<T> implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
// host.switchToWs(); // 获取WebSockets上下文
const rtx = host.switchToHttp(); // 获取http请求上下文
const response = rtx.getResponse<Response>(); // 获取上下文的response对象
const request = rtx.getRequest<Request>(); // 获取request对象
const status = exception.getStatus(); // 获取状态码
// 获取异常信息
const excepResp = exception.getResponse();
response
.status(status)
.json({
data: {},
msg: excepResp || `${status >= 500 ? 'Service Error' : 'Client Error'}`,
ret: -1,
path: request.url
});
}
}
- 绑定过滤器
// main.ts
import { HttpExceptionFilter } from './common/filter/http-exception.filter';
...
async function bootstrap() {
...
app.useGlobalFilters(new HttpExceptionFilter())
...
}
绑定后再试下一下,发现我们抛出的异常返回格式已经变了。
虽然添加了一个Http过滤器,但面前缺少参数导致报的500错误并没有解决,这样因为那不属于Http异常,可以通过创建一个可以拦截所有异常的过滤器(@Catch不传参)。绑定拦截器的范围也有多种:方法范围,控制器范围或全局范围。更多信息请看文档
拦截器
前面通过HttpException异常过滤器,将Http异常请求做了统一的返回格式。那是不是可以让正常的请求也有个统一的返回格式呢,这时就可以使用拦截器了。
拦截器是使用@Injectable()装饰器注解的类。拦截器应该实现NestInterceptor接口。
拦截器具有一系列有用的功能,这些功能受面向切面编程(AOP)技术的启发。它们可以:
- 在函数执行之前/之后绑定额外的逻辑
- 转换从函数返回的结果
- 转换从函数抛出的异常
- 扩展基本函数行为
- 根据所选条件完全重写函数 (例如, 缓存目的)
创建一个拦截器文件src/common/interceptor/transform.interceptor.ts。编码如下
// transform.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { map, Observable } from 'rxjs';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data)=>{
return {
data,
msg: '请求成功!',
ret: 0,
}
})
);
}
}
和异常过滤器一样,在main.ts中注册一下
// main.ts
import { TransformInterceptor } from './common/transform.interceptor';
...
async function bootstrap() {
...
app.useGlobalInterceptors(new TransformInterceptor());
...
}
效果
通过过滤器和拦截器,不管请求失败还是成功,都是统一格式的返回。
管道
管道是具有 @Injectable() 装饰器的类。管道应实现 PipeTransform 接口。
管道有两个类型:
- 转换:管道将输入数据转换为所需的数据输出
- 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常;
在这两种情况下, 管道 参数(arguments) 会由 控制器(controllers)的路由处理程序 进行处理. Nest 会在调用这个方法之前插入一个管道,管道会先拦截方法的调用参数,进行转换或是验证处理,然后用转换好或是验证好的参数调用原方法。
也就是说,可以在控制器方法调用前,通过插入管道的方式对控制器方法的参数做一些。理,然后再将处理过的参数传给控制器方法。
上面的例子,使用实体能够应对简单数据库操作,但没有对请求数据进行校验、也没有对返回数据做任何处理。这显然很不合理,比如获取用户时password最好是不返回的,而且password应加密存储。
在Nest中可以使用管道搭配数据传输对象 DTO(Data Transfer Object)实现对数据的校验功能。
数据校验
先创建src/user/dto/create-user.dto.ts文件
export class CreateUserDto {
readonly username: string;
readonly nickname: string;
readonly password: string;
readonly age: number;
}
然后给UserController的createUser方法引入该Dto
// user.controller.ts
...
@Post('create')
createUser(@Body() user: CreateUserDto) {
return this.userService.createUser(user);
}
...
nestjs内置了一些管道,接下来就用其中一个内置管道ValidationPipe结合class-validator对参数进行校验。
先安装依赖yarn add class-validator class-transformer
然后在create-user.dto.ts使用
// create-user.dto.ts
import { IsNotEmpty, IsNumber } from 'class-validator';
export class CreateUserDto {
@IsNotEmpty({ message: '名称不能为空' })
readonly username: string;
@IsNotEmpty({ message: '别名不能为空' })
readonly nickname: string;
@IsNotEmpty({ message: '密码不能为空' })
readonly password: string;
@IsNumber()
readonly age: number;
}
最后还需要main.ts中注册管道ValidationPipe
app.useGlobalPipes(new ValidationPipe());
试下效果
利用管道,轻松的解决了数据校验问题,就不用为写一堆if了。更多内容请查看中文文档管道
接口文档
写接口文档往往需要花费大量时间,费时费力还容易被吐槽文档垃圾。在Nestjs中提供了专门的模块来使用Swagger,方便我们写出合格的文档(少费时力,同事还满意)。只要注解到位,就能精确表达接口和字段含义。
使用前,先安装下依赖
yarn add @nestjs/swagger swagger-ui-express
接下来就是在main.ts配置下文档信息了
// main.ts
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
...
async function bootstrap() {
...
// 设置 swagger 文档
const config = new DocumentBuilder()
.setTitle('接口文档')
.setDescription('nestjs 接口文档')
.setVersion('1.0')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app,config);
SwaggerModule.setup('docs',app, document);
...
}
然后直接打开http://localhost:3000/docs/#/就能看Swagger生成的文档了。
虽然现在看路由信息,但没有接口描述信息。就这样交给前端怕不是会被打...
只要加上控制器描述(@ApiTags())、接口描述(@ApiOperation())、字段描述(@ApiProperty())。
// user.controller.ts
import { Body, Controller, Get, Param, Post } from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger/dist";
import { CreateUserDto } from "./dto/create-user.dto";
import { UserService } from "./user.service";
@ApiTags('用户模块')
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@ApiOperation({ summary: '查询用户' })
@Get(':username')
getUser(@Param() params) {
return this.userService.getUser(params.username);
}
@ApiOperation({ summary: '新建用户' })
@Post('create')
createUser(@Body() user: CreateUserDto) {
return this.userService.createUser(user);
}
}
// =================================================
// create-user.dto.ts
import { ApiProperty } from '@nestjs/swagger/dist/decorators';
import { IsNotEmpty, IsNumber } from 'class-validator';
export class CreateUserDto {
@ApiProperty({ description: '名称' })
@IsNotEmpty({ message: '名称不能为空' })
readonly username: string;
@ApiProperty({ description: '别名' })
@IsNotEmpty({ message: '别名不能为空' })
readonly nickname: string;
@ApiProperty({ description: '密码' })
@IsNotEmpty({ message: '密码不能为空' })
readonly password: string;
@ApiProperty({ description: '年龄' })
@IsNumber()
readonly age: number;
}
中间件
Nest官方文档对中间件介绍的很明了了,建议直接阅读官方文档。中间件是在路由处理程序 之前 调用的函数,Nest 中间件实际上等价于 express 中间件。
中间件可以是函数中间件或继承了NestMiddleware接口的类中间件,可在模块中注册中间件也可在全局注册。
- 定义方式1:实现
NestMiddleware接口的 类中间件import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; @Injectable() export class LoggerMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { console.log('Request...'); next(); } } - 定义方式2:函数中间件
export function logger(req, res, next) { console.log(`Request...`); next(); }; - 注册方式1:模块中注册
// ... export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer .apply(LoggerMiddleware) // .apply(logger) .forRoutes(中间件消费者); } } - 注册方式2:全局注册
app.use(logger) // app.use(LoggerMiddleware)
守卫
守卫是一个使用 @Injectable() 装饰器的类。 守卫应该实现 CanActivate 接口。
守卫在每个中间件之后执行,但在任何拦截器或管道之前执行。
可以通过中间件来判断某个路由是否有权限访问(即授权),同样也可以通过守卫来判断路由的权限。不同的是,中间件调用 next() 函数后会执行哪个处理程序,而守卫可以访问 ExecutionContext 实例,因此确切地知道接下来要执行什么。
注册守卫
守卫应实现CanActivate接口,有个canActivate方法同步或异步返回Boolean值。
- 返回true:表示通过则会继续该请求
- 返回false:表示不通过会忽略当前请求,并抛出一个
HttpException异常
例如注册一个直接返回true的守卫,即所有请求都通过,实际开发可根据具体权限逻辑返回Boolean值
// test.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class TestGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}
绑定守卫
需绑定才能使用守卫,与管道和异常过滤器一样,守卫可以是控制范围的、方法范围的或全局范围的。
-
控制器范围
@Controller() @UseGuards(TestGuard) export class AppController { // ... } -
方法范围
@Controller() export class AppController { // ... @Get() @UseGuards(TestGuard) getHello(): string { console.log('infos', this.infos); return this.appService.getHello(); } } -
全局范围
app.useGlobalGuards(new TestGuard());
因守卫直接返回的true,请求被正常处理。
但返回false时,请抛出一个 HttpException 异常。
利用守卫,还可以根据角色权限限制接口的请求。更多详情内容可查看中文官网守卫
结尾
目前只是简单介绍了下nestjs基础内容,写的不好的地方还望海涵,如有不对的地方欢迎指出[抱拳]