前边我们已经搭建了一个Nest项目,现在我们来看一下怎么写一个CRUD的项目。
NestJs基础使用 我们通过运行npm run start:dev将搭建好的NestJs项目运行起来,然后看一下main.ts的内容:
1 2 3 4 5 6 7 8 9 import { NestFactory } from '@nestjs/core' import { AppModule } from './app.module' async function bootstrap ( ) { const app = await NestFactory.create(AppModule) await app.listen(3000 ) } bootstrap()
其他内容看不懂可以不关注,只看一行await app.listen(3000)我们可以知道服务是运行在3000端口上的,此时我们通过postman访问localhost:3000看一下:
服务返回了一句字符串Hello World!,我们看一下app.controller.ts和app.service.ts,可以知道这个请求被app.controller.ts处理的,在内部调用了app.service.ts中的方法获得内容,这是一个解耦的做法,所以在后边我们也一样在controller中处理请求,在service中来处理逻辑。
我们的接口的路径肯定不能都是/,所以需要定义,在app.controller.ts中进行如下修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 import { Controller, Get } from '@nestjs/common' import { AppService } from './app.service' @Controller ('app' )export class AppController { constructor (private readonly appService: AppService ) {} @Get ('hello' ) getHello(): string { return this .appService.getHello() } }
可以发现我们在@Controller和@Get两个装饰器中添加了内容:
@Controller('app'): 这个Controller下的所有请求都有app这个前缀
@Get('hello'): 这个请求接口的路径是hello
所以现在我们要访问这个接口应该访问的路径是http://localhost:3000/app/hello
现在我们只是控制了整个Controller的前缀,假如我们要对整个项目添加一个公共前缀比如nest怎么做呢?
全局的操作需要在main.ts中进行处理了:
1 2 3 4 5 6 7 8 9 10 11 import { NestFactory } from '@nestjs/core' import { AppModule } from './app.module' async function bootstrap ( ) { const app = await NestFactory.create(AppModule) + app.setGlobalPrefix('nest' ) await app.listen(3000 ) } bootstrap()
现在我们就需要通过http://localhost:3000/nest/app/hello来访问之前的那个接口了。
搭建CRUD代码结构 我们可以通过nest g res 模块名快速生成REST CRUD代码。生成的内容默认是有测试文件的,可以添加--no-spec参数来忽略测试文件,避免文件太多干扰我这里就不生成测试文件了,在项目根目录执行nest g res books --no-spec命令,选择REST API和生成entity,我们会看到在src目录下生成了一个books目录,其内容如下:
1 2 3 4 5 6 7 8 9 books | -- books.module.ts | -- books.controller.ts | -- books.service.ts | -- dto | | -- create-book.dto.ts | | -- update-book.dto.ts | -- entities | | -- book.entity.ts
而且我们此时去查看app.module.ts的内容,发现我们新建的books模块已经自动引入了:
1 2 3 4 5 6 7 8 9 10 11 12 import { Module } from '@nestjs/common' import { AppController } from './app.controller' import { AppService } from './app.service' import { BooksModule } from './books/books.module' @Module ({ imports : [BooksModule], controllers : [AppController], providers : [AppService], }) export class AppModule {}
目前books.controller.ts与books.service.ts都已经有一些预置内容了,Nestjs帮我们生成了REST API,但是在这里为了后续的内容讲解并不准备用REST API,而是只用GET和POST请求来实现逻辑,所以我们将这两个文件的内容做一些删减变成如下模样,后边我们自己来实现CRUD接口:
1 2 3 4 5 6 7 8 9 10 import { Controller, Get, Post, Body } from '@nestjs/common' import { BooksService } from './books.service' import { CreateBookDto } from './dto/create-book.dto' import { UpdateBookDto } from './dto/update-book.dto' @Controller ('books' )export class BooksController { constructor (private readonly booksService: BooksService ) {} }
1 2 3 4 5 6 7 import { Injectable } from '@nestjs/common' import { CreateBookDto } from './dto/create-book.dto' import { UpdateBookDto } from './dto/update-book.dto' @Injectable ()export class BooksService {}
1 2 3 4 5 6 7 8 9 10 import { Module } from '@nestjs/common' import { BooksService } from './books.service' import { BooksController } from './books.controller' @Module ({ controllers : [BooksController], providers : [BooksService], }) export class BooksModule {}
如果你想使用REST API,那么在这个生成代码的基础上直接扩展是更方便的
Mysql和Typeorm引入 接下来我们先不急于去实现我们的CRUD接口,后端总是要操作数据库,所以我们先来引入数据库的配置。这里用到了Mysql和Typeorm
Typeorm后续只会有一些简单的应用,不必担心无法理解。
如果想全面了解Typeorm内容,请访问其官方文档 进行学习。
安装所需依赖:
1 npm install --save @nestjs/typeorm typeorm mysql2
接下来在app.module.ts中配置TypeOrmModule
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import { Module } from '@nestjs/common' import { TypeOrmModule } from '@nestjs/typeorm' import { AppController } from './app.controller' import { AppService } from './app.service' import { BooksModule } from './books/books.module' import { Book } from './books/entities/book.entity' @Module ({ imports : [ TypeOrmModule.forRoot({ type : 'mysql' , host : '你的ip' , port : 3306 , username : 'username' , password : 'password' , database : 'dbName' , entities : [Book], synchronize : true , }), BooksModule, ], controllers : [AppController], providers : [AppService], }) export class AppModule {}
现在我们还没有定义实体内容,等entity定义完成之后运行项目,会自动在数据库中创建book这个表。如果表不存在会自动创建,如果字段进行了修改也会自动同步
现在我们来定义实体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm' @Entity ()export class Book { @PrimaryGeneratedColumn ('uuid' ) id : string @Column () title : string @Column () anchor : string @Column ({ nullable : true , default : null , }) pageSize : number @Column ({ default : true }) isActive : boolean }
通过@Entity()装饰器声明这是一个实体,@Column()用于声明列,@PrimaryGeneratedColumn('uuid')用于声明这是一个自动生成的主键且由uuid填充。
此时等待项目运行,我们查看数据库的book这个表desc book;:
1 2 3 4 5 6 7 8 9 10 +----------+--------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +----------+--------------+------+-----+---------+-------+ | id | varchar(36) | NO | PRI | NULL | | | title | varchar(255) | NO | | NULL | | | anchor | varchar(255) | NO | | NULL | | | pageSize | int | YES | | NULL | | | isActive | tinyint | NO | | 1 | | +----------+--------------+------+-----+---------+-------+ 5 rows in set (0.00 sec)
此时表已经成功创建出来了。我们知道Typeorm可以通过实体来获取对应的存储库,通过这个存储库来进行数据库操作,我们在books.module.ts定义用注册的存储库:
1 2 3 4 5 6 7 8 9 10 11 12 import { Module } from '@nestjs/common' import { BooksService } from './books.service' import { BooksController } from './books.controller' +import { TypeOrmModule } from '@nestjs/typeorm' +import { Book } from './entities/book.entity' @Module ({+ imports: [TypeOrmModule.forFeature([Book])], controllers : [BooksController], providers : [BooksService], }) export class BooksModule {}
接下来我们可以通过@InjectRepository()装饰器将存储库注入到service中,这样我们就可以在service中进行数据库操作了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { Injectable } from '@nestjs/common' +import { InjectRepository } from '@nestjs/typeorm' +import { Repository } from 'typeorm' import { CreateBookDto } from './dto/create-book.dto' import { UpdateBookDto } from './dto/update-book.dto' +import { Book } from './entities/book.entity' @Injectable ()export class BooksService {+ constructor ( + @InjectRepository (Book) + private booksRepository: Repository<Book>, + ) {}}
CRUD操作
写在最前边,这里我们不考虑各种校验等内容,只是把CRUD操作完成
新增数据 在新增之前我们实现一下create-book.dto.ts
1 2 3 4 5 6 7 8 9 export class CreateBookDto { title : string anchor : string pageSize : number isActive : boolean }
在books.controller.ts中添加接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { Controller, Get, Post, Body } from '@nestjs/common' import { BooksService } from './books.service' import { CreateBookDto } from './dto/create-book.dto' import { UpdateBookDto } from './dto/update-book.dto' @Controller ('books' )export class BooksController { constructor (private readonly booksService: BooksService ) {} + @Post ('create' ) + createBook (@Body () createBookDto: CreateBookDto ) { + return this .booksService.createBook(createBookDto) + } }
在books.service.ts进行处理:
typeorm可以通过save()创建数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { Injectable } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' import { Repository } from 'typeorm' import { CreateBookDto } from './dto/create-book.dto' import { UpdateBookDto } from './dto/update-book.dto' import { Book } from './entities/book.entity' @Injectable ()export class BooksService { constructor ( @InjectRepository (Book) private booksRepository: Repository<Book>, ) {}+ createBook (createBookDto: CreateBookDto ) { + return this .booksRepository.save(createBookDto) + } }
此时我们使用postman访问http://localhost:3000/nest/books/create接口发送POST请求,请求体如下:
1 2 3 4 5 6 { "title" : "三国演义" , "anchor" : "罗贯中" , "pageSize" : 30 , "isActive" : true }
响应结果为:
1 2 3 4 5 6 7 { "title" : "三国演义" , "anchor" : "罗贯中" , "pageSize" : 30 , "isActive" : true , "id" : "096dd811-770d-49bb-a160-be11813e6942" }
此时我们去数据库进行查询
1 2 3 4 5 6 7 mysql> select * from book; + | id | title | anchor | pageSize | isActive | + | 096 dd811-770 d-49 bb- a160- be11813e6942 | 三国演义 | 罗贯中 | 30 | 1 | + 1 row in set (0.00 sec)
查询数据 在进行查询之前,我们往数据库又添加了几条数据:
1 2 3 4 5 6 7 8 + | id | title | anchor | pageSize | isActive | + | 096 dd811-770 d-49 bb- a160- be11813e6942 | 三国演义 | 罗贯中 | 30 | 1 | | 5176 c094-7 c78-4 aaf- ba16-488965583 d09 | 西游记 | 吴承恩 | 300 | 1 | | 95 a5143d- f521-42 ae-88 cc- e5b09943db34 | 红楼梦 | 曹雪芹 | 300 | 1 | | 9617e315 -1 f6f-48 f9- b9b0-2 ad9baec26ba | 水浒传 | 施耐庵 | 300 | 1 | +
在books.controller.ts添加接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import { Controller, Get, Post, Body, Param } from '@nestjs/common' import { BooksService } from './books.service' import { CreateBookDto } from './dto/create-book.dto' import { UpdateBookDto } from './dto/update-book.dto' @Controller ('books' )export class BooksController { constructor (private readonly booksService: BooksService ) {} @Post ('create' ) createBook (@Body () createBookDto: CreateBookDto ) { return this .booksService.createBook(createBookDto) } + @Get ('findAll' ) + findAllBook ( ) { + return this .booksService.findAllBook() + } + @Get ('findOne/:id' ) + findOneBook (@Param ('id' ) id: string ) { + return this .booksService.findOneBook(id) + } }
我们通过@Param装饰器来接收Param参数,如果有多个可以分开定义:
@Get(‘findOne/:id/:name’)
findOneBook(@Param(‘id’) id: string, @Param(‘name’) name: string) {}
@Param接收两个参数,第一个参数是字段名,第二个是可选字段,用来传递管道
在books.service.ts添加处理逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import { Injectable } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' import { Repository } from 'typeorm' import { CreateBookDto } from './dto/create-book.dto' import { UpdateBookDto } from './dto/update-book.dto' import { Book } from './entities/book.entity' @Injectable ()export class BooksService { constructor ( @InjectRepository (Book) private booksRepository: Repository<Book>, ) {} createBook (createBookDto: CreateBookDto ) { return this .booksRepository.save(createBookDto) } + findAllBook ( ) { + return this .booksRepository.find() + } + findOneBook (id: string ) { + return this .booksRepository.findOneBy({ + id, + }) + } }
调用http://localhost:3000/nest/books/findAll接口可得:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 [ { "id" : "096dd811-770d-49bb-a160-be11813e6942" , "title" : "三国演义" , "anchor" : "罗贯中" , "pageSize" : 30 , "isActive" : true }, { "id" : "5176c094-7c78-4aaf-ba16-488965583d09" , "title" : "西游记" , "anchor" : "吴承恩" , "pageSize" : 300 , "isActive" : true }, { "id" : "95a5143d-f521-42ae-88cc-e5b09943db34" , "title" : "红楼梦" , "anchor" : "曹雪芹" , "pageSize" : 300 , "isActive" : true }, { "id" : "9617e315-1f6f-48f9-b9b0-2ad9baec26ba" , "title" : "水浒传" , "anchor" : "施耐庵" , "pageSize" : 300 , "isActive" : true } ]
调用http://localhost:3000/nest/books/findOne/5176c094-7c78-4aaf-ba16-488965583d09接口可得:
1 2 3 4 5 6 7 { "id" : "5176c094-7c78-4aaf-ba16-488965583d09" , "title" : "西游记" , "anchor" : "吴承恩" , "pageSize" : 300 , "isActive" : true }
没有实现分页操作,分页可以通过findBy传入分页条件进行查询
更新数据 在更新操作之前也完善一下update-book.dto.ts
1 2 3 4 5 6 7 8 9 import { PartialType } from '@nestjs/mapped-types' import { CreateBookDto } from './create-book.dto' export class UpdateBookDto extends PartialType (CreateBookDto ) { id : string }
在books.controller.ts增加接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { Controller, Get, Post, Body, Param } from '@nestjs/common' import { BooksService } from './books.service' import { CreateBookDto } from './dto/create-book.dto' import { UpdateBookDto } from './dto/update-book.dto' @Controller ('books' )export class BooksController { constructor (private readonly booksService: BooksService ) {} + @Post ('update' ) + updateBook (@Body () updateBookDto: UpdateBookDto ) { + return this .booksService.updateBook(updateBookDto) + } }
在books.service.ts中操作数据库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { Injectable } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' import { Repository } from 'typeorm' import { CreateBookDto } from './dto/create-book.dto' import { UpdateBookDto } from './dto/update-book.dto' import { Book } from './entities/book.entity' @Injectable ()export class BooksService { constructor ( @InjectRepository (Book) private booksRepository: Repository<Book>, ) {} + updateBook (updateBookDto: UpdateBookDto ) { + return this .booksRepository.save(updateBookDto) + } }
我们访问http://localhost:3000/nest/books/update并传入数据:
1 2 3 4 5 6 7 { "id" : "5176c094-7c78-4aaf-ba16-488965583d09" , "title" : "道德经" , "anchor" : "老子" , "pageSize" : 300 , "isActive" : true }
此时我们查询数据库内容:
1 2 3 4 5 6 7 8 9 10 mysql> select * from book; + | id | title | anchor | pageSize | isActive | + | 096 dd811-770 d-49 bb- a160- be11813e6942 | 三国演义 | 罗贯中 | 30 | 1 | | 5176 c094-7 c78-4 aaf- ba16-488965583 d09 | 道德经 | 老子 | 300 | 1 | | 95 a5143d- f521-42 ae-88 cc- e5b09943db34 | 红楼梦 | 曹雪芹 | 300 | 1 | | 9617e315 -1 f6f-48 f9- b9b0-2 ad9baec26ba | 水浒传 | 施耐庵 | 300 | 1 | + 4 rows in set (0.00 sec)
这里我们update的时候传递的是全量数据,所以直接save就可以了。
如果我们update只传递了几个字段,那么就需要通过id找到数据库中的数据,再把对应的字段进行修改,然后将修改后的数据进行save
删除数据 在books.controller.ts增加接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { Controller, Get, Post, Body, Param } from '@nestjs/common' import { BooksService } from './books.service' import { CreateBookDto } from './dto/create-book.dto' import { UpdateBookDto } from './dto/update-book.dto' @Controller ('books' )export class BooksController { constructor (private readonly booksService: BooksService ) {} + @Get ('delete/:id' ) + deleteBook (@Param ('id' ) id: string ) { + return this .booksService.deleteBook(id) + } }
在books.service.ts添加逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { Injectable } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' import { Repository } from 'typeorm' import { CreateBookDto } from './dto/create-book.dto' import { UpdateBookDto } from './dto/update-book.dto' import { Book } from './entities/book.entity' @Injectable ()export class BooksService { constructor ( @InjectRepository (Book) private booksRepository: Repository<Book>, ) {} + deleteBook (id: string ) { + return this .booksRepository.delete(id) + } }
我们请求http://localhost:3000/nest/books/delete/5176c094-7c78-4aaf-ba16-488965583d09来删除名为道德经的数据,此时查询数据库:
1 2 3 4 5 6 7 8 9 mysql> select * from book; + | id | title | anchor | pageSize | isActive | + | 096 dd811-770 d-49 bb- a160- be11813e6942 | 三国演义 | 罗贯中 | 30 | 1 | | 95 a5143d- f521-42 ae-88 cc- e5b09943db34 | 红楼梦 | 曹雪芹 | 300 | 1 | | 9617e315 -1 f6f-48 f9- b9b0-2 ad9baec26ba | 水浒传 | 施耐庵 | 300 | 1 | + 3 rows in set (0.00 sec)
这里用的是真删除,实际生产中请考虑需求,一般来说都是做逻辑删除的
在上边的CRUD示例中,展示了几个常用的typeorm操作,其实还有很多api,在使用过程中自己查询文档吧。
字段验证 前边的C和U操作,请求体我们并没有进行校验,这里我们简单提一下,Nestjs的管道操作配合class-validator可以很好的帮我们实现校验功能
安装依赖:
1 npm i --save class-validator class-transformer
我们对create-book.dto.ts中进行字段约束
1 2 3 4 5 6 7 8 9 10 11 12 +import { IsNotEmpty, IsString, IsOptional } from 'class-validator' export class CreateBookDto {+ @IsString ({ message : '标题必须是字符串' }) + @IsNotEmpty ({ message : '标题不能为空' }) title : string + @IsOptional () + @IsString ({ message : '作者名是字符串' }) anchor : string pageSize : number isActive : boolean }
注意我们有一个IsOptional()装饰器,该装饰器可以把这个字段标记为可选的,即anchor===null或者anchor===undefined的话,就会忽略其他的验证规则,如果anchor有值则必须为string类型
我们不能通过anchor?: string这种方式来标记可选字段
然后我们在当前目录下新建一个管道validate.pipe.ts
关于管道的内容这里不展开说,留到后边再讲,这里主要是对dto的字段校验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException, } from '@nestjs/common' import { validate } from 'class-validator' import { plainToInstance } from 'class-transformer' @Injectable ()export class ValidationPipe implements PipeTransform <any > { async transform (value: any , { metatype }: ArgumentMetadata ) { if (!metatype || !this .toValidate(metatype)) { return value } const object = plainToInstance(metatype, value) const errors = await validate(object ) if (errors.length > 0 ) { const message = errors[0 ].constraints const info = message[Object .keys(message)[0 ]] throw new BadRequestException(info ? info : 'Validation failed' ) } return value } private toValidate(metatype: any ): boolean { const types: any [] = [String , Boolean , Number , Array , Object ] return !types.includes(metatype) } }
这里我们是使用官方提供的示例自定义管道,其实Nestjs内置了一个ValidationPipe管道,功能很强大,放到后边管道章节说(验证的内容挺多的,也可能单独拿出来说)
在books.controller.ts中引入这个验证管道并应用于新增接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { Controller, Get, Post, Body, Param } from '@nestjs/common' import { BooksService } from './books.service' import { CreateBookDto } from './dto/create-book.dto' import { UpdateBookDto } from './dto/update-book.dto' +import { ValidationPipe } from './validate.pipe' @Controller ('books' )export class BooksController { constructor (private readonly booksService: BooksService ) {} @Post ('create' ) + createBook (@Body (new ValidationPipe()) createBookDto: CreateBookDto ) { return this .booksService.createBook(createBookDto) } }
此时我们访问新增接口不传递title字段:
1 2 3 4 { "anchor" : "张三" , "pageSize" : 300 }
得到的返回结果是:
1 2 3 4 5 { "statusCode" : 400 , "message" : "标题不能为空" , "error" : "Bad Request" }
我们也可以传递一个不是字符串类型的title字段进行测试:
1 2 3 4 5 { "title" : 123 , "anchor" : "张三" , "pageSize" : 300 }
得到的结果是:
1 2 3 4 5 { "statusCode" : 400 , "message" : "标题必须是字符串" , "error" : "Bad Request" }
这样一来,我们就有了一个简单的字段必填和字段类型校验。
基本的CRUD功能已经搭建完成了,但Nestjs的能力还要强悍的多,咱们下次再见