本示例在nestjs脚手架项目基础上,进行了一些修改,并通过TypeOrm实现了数据库的增删读写操作。由于Typeorm更适合关系型数据库,本示例为简便起见,选择sqlite3作为试验数据库。对于菲关系型数据库如mongodb,则推荐使用mongoose或者typegoose等库进行数据库操作。
1.nestjs安装和快速启动
安装nestjs命令行工具
#安装cli工具$ npm i-g @nestjs/cli#新建项目$ nest new projectName#如果用yarn安装的话$ yarn global add @nestjs/cli
2.模版项目helloworld说明
不同版本的cli命令行生成的默认项目可能有所不同。这里将默认的helloworld修改为async异步形式来对nestjs方法进行说明。
2.1 app.controller.ts文件
import { Controller, Get, Inject } from '@nestjs/common';import { AppService } from './app.service';/*** 应用程序控制器,@Controller() 可以指定参数,用于定义类的父路由,如 @Controller("cat"),此时这个类的所有父路由就会成为 /cat** 被 @Controller() 修饰的类,可以通过其构造函数完成依赖注入,但依赖注入的类必须与当前类属于同一个模块*/@Controller()export class AppController {/*** 构造函数,用于注入这个类的依赖,注入类时,需要使用 @Inject() 修饰符,其参数是被注入的类的类名* 在注入被 @Injectable() 修饰的类时,可以不使用 @Inject() 修饰参数,此时依赖注器入会使用参数的类型完成注入** Tips: 这里使用 @Inject(AppService) 是为了规范代码风格*/constructor(@Inject(AppService) private readonly appService: AppService,) { }/*** @Get() 可以指定参数,用于定义方法路由,如 @Get(":id"),此时这个方法路由就会成为 /:id,即查询指定ID*/@Get()async root() {return this.appService.root();}}
2.2 app.service.ts文件
import { Injectable } from '@nestjs/common';/*** 被 @Injectable() 修饰的类,可以通过其构造函数完成依赖注入,但依赖注入的类必须与当前类属于同一个模块*/@Injectable()export class AppService {constructor() { } // 构造函数,一般用于处理依赖注入async root() {return 'Hello World!';}}
2.3 app.module.ts文件
import { Module } from '@nestjs/common';import { APP_INTERCEPTOR } from '@nestjs/core';import { TypeOrmModule } from '@nestjs/typeorm';import { CatModule } from 'cats/cat.module';import { ErrorsInterceptor } from 'common/errors.interceptor';import { AppController } from './app.controller';import { AppService } from './app.service';/*** @Module() 定义一个模块,并管理这个模块的导入集合、控制器集合、提供者集合、导出集合*/@Module({// TypeOrmModule.forRoot() 默认加载项目根目录下的 ormconfig.json 配置文件用于配置数据库连接// TypeORM 配置文件详细文档 https://typeorm.io/#/using-ormconfigimports: [TypeOrmModule.forRoot(), CatModule], // 导入其他模块的集合controllers: [AppController], // 当前模块的控制器集合providers: [{provide: APP_INTERCEPTOR,useClass: ErrorsInterceptor},AppService], // 当前模块的提供者集合exports: [], // 导出当前模块的提供者,用于被其他模块调用})export class AppModule { }
2.4 main.ts入口文件
import { NestFactory } from '@nestjs/core';import { AppModule } from './app.module';async function bootstrap() {const app = await NestFactory.create(AppModule); // 创建应用程序实例,此时所有被 AppModule 导入的其他模块的所有实例都会被加载await app.listen(3000); // 使用3000端口监听应用程序}bootstrap(); // 启动应用程序 -> localhost:3000
2.5 app.controller.spec.ts测试文件
修改了上述文件之后,命令行默认生成的测试文件也需要修改为异步的方式才能通过测试。
import { Test, TestingModule } from '@nestjs/testing';import { AppController } from './app.controller';import { AppService } from './app.service';describe('AppController', () => {let appController: AppController;beforeEach(async () => {const app: TestingModule = await Test.createTestingModule({controllers: [AppController],providers: [AppService],}).compile();appController = app.get<AppController>(AppController);});describe('get', () => {it('should return "Hello World!"', async() => {const data=await appController.root();expect(data).toBe('Hello World!');});});});
3. 通用错误处理和接口文件
在项目实践中,注入错误处理、接口等模块化文件需要重复使用。最好集中在公用模块中。在项目根目录下创建common文件夹,并新建错误处理和接口模块。
3.1 common/errors.interceptor.ts错误处理文件
import { CallHandler, ExecutionContext, HttpException, Injectable, NestInterceptor } from '@nestjs/common';import { Observable } from 'rxjs';import { catchError } from 'rxjs/operators';@Injectable()export class ErrorsInterceptor implements NestInterceptor {intercept(context: ExecutionContext, next: CallHandler): Observable<any> {// 异常拦截器,拦截每个请求中的异常,目的是将异常码和异常信息改写为 { code: xxx, message: xxx } 类型return next.handle().pipe(catchError((error, caught): any => {if (error instanceof HttpException) {return Promise.resolve({code: error.getStatus(),message: error.getResponse()});}return Promise.resolve({code: 500,message: `出现了意外错误:${error.toString()}`});}));}}
3.2 common/result.interface.ts接口文件
// 定义通用的API接口返回数据类型export interface Result {code: number;message: string;data?: any;}
4.创建数据库增删读写API文件
在根目录下新建cat文件夹,并依次创建下列文件。
4.1 cat.controller.ts
import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common';import { Result } from 'common/result.interface';import { Cat } from './cat.entity';import { CatService } from './cat.service';@Controller('cat')export class CatController {constructor(@Inject(CatService) private readonly CatService: CatService,) { }@Post()async createCat(@Body() Cat: Cat): Promise<Result> {await this.CatService.createCat(Cat);return { code: 200, message: '创建成功' };}@Delete(':id')async deleteCat(@Param('id') id: number): Promise<Result> {await this.CatService.deleteCat(id);return { code: 200, message: '删除成功' };}@Put(':id')async updateCat(@Param('id') id: number, @Body() Cat: Cat): Promise<Result> {await this.CatService.updateCat(id, Cat);return { code: 200, message: '更新成功' };}@Get(':id')async findOneCat(@Param('id') id: number): Promise<Result> {const data = await this.CatService.findOneCat(id);return { code: 200, message: '查询成功', data };}}
4.2 cat.service.ts文件
import { HttpException, Injectable } from '@nestjs/common';import { InjectRepository } from '@nestjs/typeorm';import { Repository } from 'typeorm';import { Cat } from './cat.entity';@Injectable()export class CatService {constructor(@InjectRepository(Cat) private readonly catRepo: Repository<Cat>, // 使用泛型注入对应类型的存储库实例) { }/*** 创建** @param cat Cat 实体对象*/async createCat(cat: Cat): Promise<Cat> {/*** 创建新的实体实例,并将此对象的所有实体属性复制到新实体中。 请注意,它仅复制实体模型中存在的属性。*/// this.catRepo.create(cat);// 插入数据时,删除 id,以避免请求体内传入 iddelete cat.id;return this.catRepo.save(cat);/*** 将给定实体插入数据库。与save方法不同,执行原始操作时不包括级联,关系和其他操作。* 执行快速有效的INSERT操作。不检查数据库中是否存在实体,因此如果插入重复实体,本次操作将失败。*/// await this.catRepo.insert(cat);}/*** 删除** @param id ID*/async deleteCat(id: number): Promise<void> {await this.findOneById(id);this.catRepo.delete(id);}/*** 更新** @param id ID* @param cat Cat 实体对象*/async updateCat(id: number, cat: Cat): Promise<void> {await this.findOneById(id);// 更新数据时,删除 id,以避免请求体内传入 iddelete cat.id;this.catRepo.update(id, cat);}/*** 根据ID查询** @param id ID*/async findOneCat(id: number): Promise<Cat> {return this.findOneById(id);}/*** 根据ID查询单个信息,如果不存在则抛出404异常* @param id ID*/private async findOneById(id: number): Promise<Cat> {const catInfo = await this.catRepo.findOne(id);if (!catInfo) {throw new HttpException(`指定 id=${id} 的猫猫不存在`, 404);}return catInfo;}}
4.3 cat.entity.ts数据库实体文件
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';@Entity('cat')export class Cat {/*** 自增主键*/@PrimaryGeneratedColumn({comment: '自增ID'})id: number;/*** 昵称*/@Column({comment: '昵称'})nickname: string;/*** 品种*/@Column({comment: '品种'})species: string;}
4.4 cat.module.ts模块文件
import { Module } from '@nestjs/common';import { TypeOrmModule } from '@nestjs/typeorm';import { CatController } from './cat.controller';import { Cat } from './cat.entity';import { CatService } from './cat.service';@Module({imports: [TypeOrmModule.forFeature([Cat])],controllers: [CatController],providers: [CatService],})export class CatModule { }
5.配置数据库
typeorm可以和各种数据库配合使用。为了方便调试,这里用sqlite3替换掉原案例教程中的Postgresql(否则还要装一个Postgresql数据库)。
在项目中安装sqlite3驱动。
> npm i --save sqlite3
在根目录ormconfig.json文件中进行如下配置。注意,原案例中entities的目录在src/文件夹下面,实际使用时可能会报错(如果是直接运行TypeScript不会报错,如果编译成JavaScript就会报错,后面一种情况下需要修改目录到dist或者其他编译目标文件夹下)。
{"type": "sqlite","database": "./mydb.sql","entities": ["dist/**/**.entity{.ts,.js}"],"synchronize": true,"logging": true}
6.项目依赖包
项目中依赖的包文件如下,其中nestjs cli 已经安装好了一部分,typeorm等需要手动添加。
"dependencies": {"@nestjs/common": "^6.7.2","@nestjs/core": "^6.7.2","@nestjs/platform-express": "^6.7.2","@nestjs/typeorm": "^6.2.0","mongodb": "^3.4.1","pg": "^7.16.0","reflect-metadata": "^0.1.13","rimraf": "^3.0.0","rxjs": "^6.5.3","sqlite3": "^4.1.1","typeorm": "^0.2.22"},"devDependencies": {"@nestjs/cli": "^6.9.0","@nestjs/schematics": "^6.7.0","@nestjs/testing": "^6.7.1","@types/express": "^4.17.1","@types/jest": "^24.0.18","@types/node": "^12.7.5","@types/supertest": "^2.0.8","jest": "^24.9.0","prettier": "^1.18.2","supertest": "^4.0.2","ts-jest": "^24.1.0","ts-loader": "^6.1.1","ts-node": "^8.4.1","tsconfig-paths": "^3.9.0","tslint": "^5.20.0","typescript": "^3.7.4"}
