环境搭建

安装 nestjs

  • 建议用户使用 yarn 或者 cnpm 安装依赖,因为 npm 安装依赖的时候会有很多警告,但是不影响使用。
npm i -g @nestjs/cli
# 创建项目
nest new nestjs-demo

  • 目录结构

    nestjs-demo
    ├── nest-cli.json
    ├── package.json
    ├── README.md
    ├── src
    │   ├── app.controller.spec.ts
    │   ├── app.controller.ts
    │   ├── app.module.ts
    │   ├── app.service.ts
    │   └── main.ts
    ├── test
    │   ├── app.e2e-spec.ts
    │   └── jest-e2e.json
    ├── tsconfig.build.json
    └── tsconfig.json
    
  • 核心文件

    文件名说明
    app.controller.ts单个路由的基本控制器器
    app.controller.spec.ts针对控制器的单元测试
    app.module.ts应用程序的根模块
    app.service.ts具有单一方法的基本服务
    main.ts应用程序的入口文件,它使用核心函数 NestFactory 来创建 Nest 应用程序的实例
  • 启动项目

npm run start
# 或者
yarn start


# 监控文件变化
npm run start:dev
# 或者
yarn start:dev

  • 访问 http://localhost:3000/,可以看到 Hello World!

main.ts 中只有一行代码,app.module.ts 中有一行代码,app.controller.ts 中有一行代码,app.service.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 {}

.module文件需要使用@Module装饰器来装饰,@Module装饰器接收一个对象,对象中有三个属性,importscontrollersprovidersimports是导入的模块,controllers是控制器,providers是服务。

  • providers: 服务,用来处理业务逻辑,比如数据库操作、http 请求等, 服务可以注入到控制器中使用。各个模块可以共享服务,也可以在模块中创建私有服务。
  • controllers: 控制器,用来处理路由,接收请求,返回响应,控制器可以注入服务。
  • imports: 导入的模块,可以导入其他模块的服务,也可以导入第三方模块。
  • exports: 导出的模块,可以导出服务,也可以导出第三方模块。

路由装饰器

@Controller

  • @Controller 装饰器用来创建路由控制器,接收一个字符串参数,用来指定路由的前缀,如果不传参数,则默认为根路由。
import { Controller } from '@nestjs/common'
// 访问 http://localhost:3000/user
@Controller('user')
export class UserController {}

HTTP 方法处理装饰器

  • @Get 装饰器用来创建 GET 请求的路由,接收一个字符串参数,用来指定路由的路径。
  • @Post 装饰器用来创建 POST 请求的路由,接收一个字符串参数,用来指定路由的路径。
  • @Put 装饰器用来创建 PUT 请求的路由,接收一个字符串参数,用来指定路由的路径。
  • @Patch 装饰器用来创建 PATCH 请求的路由,接收一个字符串参数,用来指定路由的路径。
  • @Delete 装饰器用来创建 DELETE 请求的路由,接收一个字符串参数,用来指定路由的路径。
import { Controller, Get } from '@nestjs/common'

@Controller('user')
export class UserController {
  // 访问 http://localhost:3000/user
  @Get()
  index() {
    return 'user index'
  }

  // 访问 http://localhost:3000/user/list
  @Get('list')
  list() {
    return 'user list'
  }

  // 访问 http://localhost:3000/user/detail
  @Get('detail')
  detail() {
    return 'user detail'
  }

  // 访问 http://localhost:3000/user/add
  @Post('add')
  add() {
    return 'user add'
  }

  // 访问 http://localhost:3000/user/update
  @Put('update')
  update() {
    return 'user update'
  }

  // 访问 http://localhost:3000/user/delete
  @Delete('delete')
  delete() {
    return 'user delete'
  }

  // 动态路由获取参数
  // 访问 http://localhost:3000/user/1
  @Get(':id')
  detail(@Param('id') id: string) {
    return `user detail ${id}`
  }
}

全局路由前缀

// main.ts
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  // 全局路由前缀, 访问 http://localhost:3000/api/user
  app.setGlobalPrefix('api')
  await app.listen(3000)
}

路由参数

  • @Param 装饰器用来获取路由参数,接收一个字符串参数,用来指定路由参数的名称。
  • @Query 装饰器用来获取查询字符串参数,接收一个字符串参数,用来指定查询字符串参数的名称。
  • @Body 装饰器用来获取请求体参数,接收一个字符串参数,用来指定请求体参数的名称。
  • @Headers 装饰器用来获取请求头参数,接收一个字符串参数,用来指定请求头参数的名称。
  • @Req 装饰器用来获取请求对象,接收一个字符串参数,用来指定请求对象的名称。
  • @Res 装饰器用来获取响应对象,接收一个字符串参数,用来指定响应对象的名称。
  • @Next 装饰器用来获取下一个中间件,接收一个字符串参数,用来指定下一个中间件的名称。
  • @Session 装饰器用来获取 session 对象,接收一个字符串参数,用来指定 session 对象的名称。
  • @UploadedFile 装饰器用来获取上传的文件,接收一个字符串参数,用来指定上传文件的名称。
  • @UploadedFiles 装饰器用来获取上传的文件数组,接收一个字符串参数,用来指定上传文件的名称。
  • @Param@Query@Body@Headers@Req@Res@Next@Session@UploadedFile@UploadedFiles 装饰器都接收一个字符串参数,用来指定参数的名称,如果不传参数,则默认使用参数的名称。

命令行工具

  • nest 命令行工具用来创建控制器、服务、模块等,使用 nest --help 可以查看帮助信息。
# 语法
nest g <schematic> <name> [options]
nest g [文件类型] [文件名称] [选项]

# 选项: --dry-run 仅显示将要生成的文件,而不实际生成文件
# 选项: --no-spec 不生成 .spec 文件
# 选项: --flat 不生成文件夹
# 选项: 目录 生成文件的目录

# 创建控制器
nest g controller user
# 简写
nest g co user

# 创建服务
nest g service user
# 简写
nest g s user

# 创建模块
nest g module user
# 简写
nest g mo user

# 创建中间件
nest g middleware user
# 简写
nest g mi user

# 创建管道
nest g pipe user
# 简写
nest g p user

# 创建过滤器
nest g filter user
# 简写
nest g f user

# 创建网关
nest g gateway user
# 简写
nest g ga user

# 创建拦截器
nest g interceptor user
# 简写
nest g in user

# 创建装饰器
nest g decorator user
# 简写
nest g d user

# 创建守卫
nest g guard user
# 简写
nest g gu user

# 创建解析器
nest g resolver user
# 简写
nest g r user

# 创建接口
nest g interface user
# 简写
nest g itf user

# 创建资源
nest g resource user
# 简写
nest g res user

# 创建配置
nest g configuration user
# 简写
nest g config user

# 创建类
nest g class user
# 简写
nest g cl user

连接 Mysql

安装依赖

npm i --save @nestjs/typeorm typeorm mysql2

配置数据库

  • app.module.ts 中导入 TypeOrmModule.forRoot() 方法,该方法接收一个对象,对象中有三个属性,typehostportusernamepassworddatabaseentitiessynchronizeloggingloggerautoLoadEntitieskeepConnectionAliveextra

  • 首先在项目根目录下创建两个文件.env.env.prod,分别存的是开发环境和线上环境不同的环境变量:

// 数据库地址
DB_HOST=localhost
// 数据库端口
DB_PORT=3306
// 数据库登录名
DB_USER=root
// 数据库登录密码
DB_PASSWD=root
// 数据库名字
DB_DATABASE=blog

.env.prod中的是上线要用的数据库信息,如果你的项目要上传到线上管理,为了安全性考虑,建议这个文件添加到.gitignore中。

接着在根目录下创建一个文件夹config(与src同级),然后再创建一个env.ts用于根据不同环境读取相应的配置文件。

import * as fs from 'fs'
import * as path from 'path'
const isProd = process.env.NODE_ENV === 'production'

function parseEnv() {
  const localEnv = path.resolve('.env')
  const prodEnv = path.resolve('.env.prod')

  if (!fs.existsSync(localEnv) && !fs.existsSync(prodEnv)) {
    throw new Error('缺少环境配置文件')
  }

  const filePath = isProd && fs.existsSync(prodEnv) ? prodEnv : localEnv
  return { path: filePath }
}
export default parseEnv()
  • app.module.ts中连接数据库
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigService, ConfigModule } from '@nestjs/config';
import envConfig from '../config/env';

@Module({
  imports: [
    ConfigModule.forRoot({
    isGlobal: true,  // 设置为全局
    envFilePath: [envConfig.path]
   }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      autoLoadEntities: true, // 自动加载实体
      useFactory: async (configService: ConfigService) => ({
        type: 'mysql', // 数据库类型
        entities: [
          __dirname + '/**/*.entity{.ts,.js}',
        ],  // 数据表实体
        host: configService.get('DB_HOST', 'localhost'), // 主机,默认为localhost
        port: configService.get<number>('DB_PORT', 3306), // 端口号
        username: configService.get('DB_USER', 'root'),   // 用户名
        password: configService.get('DB_PASSWORD', 'root'), // 密码
        database: configService.get('DB_DATABASE', 'blog'), //数据库名
        timezone: '+08:00', //服务器上配置的时区
        synchronize: true, //根据实体自动创建数据库表, 生产环境建议关闭
      }),
    }),
    PostsModule,
  ],
 ...
})
export class AppModule {}
  • 另外一种方法使用 ormconfig.json 方式

在根目录下创建一个ormconfig.json文件(与src同级), 而不是将配置对象传递给forRoot()的方式。

import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'

@Module({
  imports: [TypeOrmModule.forRoot()],
})
export class AppModule {}

创建实体

  • 创建实体文件 src/entity/posts.entity.ts
//src/entity/posts.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'

@Entity('posts')
export class PostsEntity {
  @PrimaryGeneratedColumn()
  id: number // 标记为主列,值自动生成

  @Column({ length: 50 })
  title: string

  @Column({ length: 20 })
  author: string

  @Column('text')
  content: string

  @Column({ default: '' })
  thumb_url: string

  @Column('tinyint')
  type: number

  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  create_time: Date

  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  update_time: Date
}
  • posts.service.ts中使用实体
import { HttpException, Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { getRepository, Repository } from 'typeorm'
import { PostsEntity } from './posts.entity'

export interface PostsRo {
  list: PostsEntity[]
  count: number
}
@Injectable()
export class PostsService {
  constructor(
    @InjectRepository(PostsEntity)
    private readonly postsRepository: Repository<PostsEntity>
  ) {}

  // 创建文章
  async create(post: Partial<PostsEntity>): Promise<PostsEntity> {
    const { title } = post
    if (!title) {
      throw new HttpException('缺少文章标题', 401)
    }
    const doc = await this.postsRepository.findOne({ where: { title } })
    if (doc) {
      throw new HttpException('文章已存在', 401)
    }
    return await this.postsRepository.save(post)
  }

  // 获取文章列表
  async findAll(query): Promise<PostsRo> {
    const qb = await getRepository(PostsEntity).createQueryBuilder('post')
    qb.where('1 = 1')
    qb.orderBy('post.create_time', 'DESC')

    const count = await qb.getCount()
    const { pageNum = 1, pageSize = 10, ...params } = query
    qb.limit(pageSize)
    qb.offset(pageSize * (pageNum - 1))

    const posts = await qb.getMany()
    return { list: posts, count: count }
  }

  // 获取指定文章
  async findById(id): Promise<PostsEntity> {
    return await this.postsRepository.findOne(id)
  }

  // 更新文章
  async updateById(id, post): Promise<PostsEntity> {
    const existPost = await this.postsRepository.findOne(id)
    if (!existPost) {
      throw new HttpException(`id为${id}的文章不存在`, 401)
    }
    const updatePost = this.postsRepository.merge(existPost, post)
    return this.postsRepository.save(updatePost)
  }

  // 刪除文章
  async remove(id) {
    const existPost = await this.postsRepository.findOne(id)
    if (!existPost) {
      throw new HttpException(`id为${id}的文章不存在`, 401)
    }
    return await this.postsRepository.remove(existPost)
  }
}
  • posts.controller.ts中使用实体
import { PostsService, PostsRo } from './posts.service'
import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Post,
  Put,
  Query,
} from '@nestjs/common'

@Controller('post')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  /**
   * 创建文章
   * @param post
   */
  @Post()
  async create(@Body() post) {
    return await this.postsService.create(post)
  }

  /**
   * 获取所有文章
   */
  @Get()
  async findAll(@Query() query): Promise<PostsRo> {
    return await this.postsService.findAll(query)
  }

  /**
   * 获取指定文章
   * @param id
   */
  @Get(':id')
  async findById(@Param('id') id) {
    return await this.postsService.findById(id)
  }

  /**
   * 更新文章
   * @param id
   * @param post
   */
  @Put(':id')
  async update(@Param('id') id, @Body() post) {
    return await this.postsService.updateById(id, post)
  }

  /**
   * 删除
   * @param id
   */
  @Delete('id')
  async remove(@Param('id') id) {
    return await this.postsService.remove(id)
  }
}

接口格式统一

  • 定义正确是返回json
{
  "code": 0,
  "message": "success",
  "data": {}
}

- 定义错误时返回`json`

```json
{
  "code": -1,
  "message": "error",
  "data": {}
}
  • 拦截请求错误 创建一个过滤器
nest g filter core/filter/http-exception
import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
} from '@nestjs/common'

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp() // 获取请求上下文
    const response = ctx.getResponse() // 获取请求上下文中的 response对象
    const status = exception.getStatus() // 获取异常状态码

    // 设置错误信息
    const message = exception.message
      ? exception.message
      : `${status >= 500 ? 'Service Error' : 'Client Error'}`
    const errorResponse = {
      data: {},
      message: message,
      code: -1,
    }

    // 设置返回的状态码, 请求头,发送错误信息
    response.status(status)
    response.header('Content-Type', 'application/json; charset=utf-8')
    response.send(errorResponse)
  }
}
  • main.ts中使用过滤器
...
import { TransformInterceptor } from './core/interceptor/transform.interceptor';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  ...
   // 注册全局错误的过滤器
  app.useGlobalInterceptors(new TransformInterceptor());
  await app.listen(3000);
}
bootstrap();
  • 拦截成功请求 创建一个拦截器
nest g interceptor core/interceptor/transform
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,
          code: 0,
          msg: '请求成功',
        }
      })
    )
  }
}
  • main.ts中使用拦截器
...
import { TransformInterceptor } from './core/interceptor/transform.interceptor';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  ...
  // 全局注册拦截器
 app.useGlobalInterceptors(new TransformInterceptor())
  await app.listen(9080);
}
bootstrap();

使用 Swagger

  • 安装依赖
npm i --save @nestjs/swagger swagger-ui-express

  • main.ts中使用 Swagger
...
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  ...
  // 设置swagger文档
  const config = new DocumentBuilder()
    .setTitle('管理后台')
    .setDescription('管理后台接口文档')
    .setVersion('1.0')
    .addBearerAuth()
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('docs', app, document);

  await app.listen(3000);
}
bootstrap();
  • 配置完成 Swagger 后,访问 http://localhost:3000/docs,可以看到 Swagger 文档。

接口标签

  • 根据Controller来分类, 只要添加@ApiTags就可以
...
import { ApiTags } from '@nestjs/swagger';
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';

@ApiTags("文章")
@Controller('post')
export class PostsController {...}

接口说明

  • 同样在 Controller 中, 在每一个路由的前面使用@ApiOperation 装饰器

    //  posts.controller.ts
    ...
    import { ApiTags,ApiOperation } from '@nestjs/swagger';
    export class PostsController {
    
    @ApiOperation({ summary: '创建文章' })
    @Post()
    async create(@Body() post) {....}
    
    @ApiOperation({ summary: '获取文章列表' })
    @Get()
    async findAll(@Query() query): Promise<PostsRo> {...}
    ....
    }
    

接口传参

posts目录下创建一个 dto 文件夹,再创建一个create-post.dot.ts文件

// dto/create-post.dot.ts
export class CreatePostDto {
  readonly title: string
  readonly author: string
  readonly content: string
  readonly cover_url: string
  readonly type: number
}

然后在Controller中对创建文章是传入的参数进行类型说明

//  posts.controller.ts
...
import { CreatePostDto } from './dto/create-post.dto';

@ApiOperation({ summary: '创建文章' })
@Post()
async create(@Body() post:CreatePostDto) {...}

create-post.dto 文件添加 Swagger 的注释

import { ApiProperty } from '@nestjs/swagger'

export class CreatePostDto {
  @ApiProperty({ description: '文章标题' })
  readonly title: string

  @ApiProperty({ description: '作者' })
  readonly author: string

  @ApiPropertyOptional({ description: '内容' })
  readonly content: string

  @ApiPropertyOptional({ description: '文章封面' })
  readonly cover_url: string

  @ApiProperty({ description: '文章类型' })
  readonly type: number
}

数据验证

安装依赖

npm i --save class-validator class-transformer

使用验证器

  • create-post.dto.ts中使用验证器
import { IsNotEmpty, IsNumber, IsString } from 'class-validator'

export class CreatePostDto {
  @ApiProperty({ description: '文章标题' })
  @IsNotEmpty({ message: '文章标题必填' })
  readonly title: string

  @IsNotEmpty({ message: '缺少作者信息' })
  @ApiProperty({ description: '作者' })
  readonly author: string

  @ApiPropertyOptional({ description: '内容' })
  readonly content: string

  @ApiPropertyOptional({ description: '文章封面' })
  readonly cover_url: string

  @IsNumber()
  @ApiProperty({ description: '文章类型' })
  readonly type: number
}

posts.controller.ts 中使用验证器


import { CreatePostDto } from './dto/create-post.dto';
import { PostsService, PostsRo } from './posts.service';
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';

@Controller('post')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  /**
   * 创建文章
   * @param post
   */
  @Post()
  async create(@Body() post:CreatePostDto) {
    return await this.postsService.create(post)
  }
  ...
}

main.ts 中全局注册一下管道

...
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  ...
  // 全局注册管道
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}

bootstrap();

中间件

  • 创建中间件
nest g middleware core/middleware/logger
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()
  }
}
  • app.module.ts中使用中间件
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'
...
import { LoggerMiddleware } from './core/middleware/logger.middleware'

@Module({
  imports: [...],
  controllers: [...],
  providers: [...],
})

export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('*')
  }
}

注意: 在全局中间件只能使用 函数式中间件,不能使用类中间件。

守卫

  • 创建守卫
nest g guard core/guard/auth
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { Observable } from 'rxjs'

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest()
    console.log('request', request)
    return validateRequest(request) // 校验请求
  }
}
  • controller 中使用守卫
...

import { AuthGuard } from './core/guard/auth.guard'

@Controller('post')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  /**
   * 创建文章
   * @param post
   */
  @Post()
  @UseGuards(AuthGuard)
  async create(@Body() post: CreatePostDto) {
    return await this.postsService.create(post)
  }
  ...
}

参考资料