环境搭建
安装 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
装饰器接收一个对象,对象中有三个属性,imports
、controllers
、providers
,imports
是导入的模块,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()
方法,该方法接收一个对象,对象中有三个属性,type
、host
、port
、username
、password
、database
、entities
、synchronize
、logging
、logger
、autoLoadEntities
、keepConnectionAlive
、extra
。首先在项目根目录下创建两个文件
.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)
}
...
}