本示例在nestjs脚手架项目基础上,进行了一些修改,并通过TypeOrm实现了数据库的增删读写操作。由于Typeorm更适合关系型数据库,本示例为简便起见,选择sqlite3作为试验数据库。对于菲关系型数据库如mongodb,则推荐使用mongoose或者typegoose等库进行数据库操作。

1.nestjs安装和快速启动

安装nestjs命令行工具

  1. #安装cli工具
  2. $ npm i-g @nestjs/cli
  3. #新建项目
  4. $ nest new projectName
  5. #如果用yarn安装的话
  6. $ yarn global add @nestjs/cli

2.模版项目helloworld说明

不同版本的cli命令行生成的默认项目可能有所不同。这里将默认的helloworld修改为async异步形式来对nestjs方法进行说明。

2.1 app.controller.ts文件

  1. import { Controller, Get, Inject } from '@nestjs/common';
  2. import { AppService } from './app.service';
  3. /**
  4. * 应用程序控制器,@Controller() 可以指定参数,用于定义类的父路由,如 @Controller("cat"),此时这个类的所有父路由就会成为 /cat
  5. *
  6. * 被 @Controller() 修饰的类,可以通过其构造函数完成依赖注入,但依赖注入的类必须与当前类属于同一个模块
  7. */
  8. @Controller()
  9. export class AppController {
  10. /**
  11. * 构造函数,用于注入这个类的依赖,注入类时,需要使用 @Inject() 修饰符,其参数是被注入的类的类名
  12. * 在注入被 @Injectable() 修饰的类时,可以不使用 @Inject() 修饰参数,此时依赖注器入会使用参数的类型完成注入
  13. *
  14. * Tips: 这里使用 @Inject(AppService) 是为了规范代码风格
  15. */
  16. constructor(
  17. @Inject(AppService) private readonly appService: AppService,
  18. ) { }
  19. /**
  20. * @Get() 可以指定参数,用于定义方法路由,如 @Get(":id"),此时这个方法路由就会成为 /:id,即查询指定ID
  21. */
  22. @Get()
  23. async root() {
  24. return this.appService.root();
  25. }
  26. }

2.2 app.service.ts文件

  1. import { Injectable } from '@nestjs/common';
  2. /**
  3. * 被 @Injectable() 修饰的类,可以通过其构造函数完成依赖注入,但依赖注入的类必须与当前类属于同一个模块
  4. */
  5. @Injectable()
  6. export class AppService {
  7. constructor() { } // 构造函数,一般用于处理依赖注入
  8. async root() {
  9. return 'Hello World!';
  10. }
  11. }

2.3 app.module.ts文件

  1. import { Module } from '@nestjs/common';
  2. import { APP_INTERCEPTOR } from '@nestjs/core';
  3. import { TypeOrmModule } from '@nestjs/typeorm';
  4. import { CatModule } from 'cats/cat.module';
  5. import { ErrorsInterceptor } from 'common/errors.interceptor';
  6. import { AppController } from './app.controller';
  7. import { AppService } from './app.service';
  8. /**
  9. * @Module() 定义一个模块,并管理这个模块的导入集合、控制器集合、提供者集合、导出集合
  10. */
  11. @Module({
  12. // TypeOrmModule.forRoot() 默认加载项目根目录下的 ormconfig.json 配置文件用于配置数据库连接
  13. // TypeORM 配置文件详细文档 https://typeorm.io/#/using-ormconfig
  14. imports: [TypeOrmModule.forRoot(), CatModule], // 导入其他模块的集合
  15. controllers: [AppController], // 当前模块的控制器集合
  16. providers: [
  17. {
  18. provide: APP_INTERCEPTOR,
  19. useClass: ErrorsInterceptor
  20. },
  21. AppService
  22. ], // 当前模块的提供者集合
  23. exports: [], // 导出当前模块的提供者,用于被其他模块调用
  24. })
  25. export class AppModule { }

2.4 main.ts入口文件

  1. import { NestFactory } from '@nestjs/core';
  2. import { AppModule } from './app.module';
  3. async function bootstrap() {
  4. const app = await NestFactory.create(AppModule); // 创建应用程序实例,此时所有被 AppModule 导入的其他模块的所有实例都会被加载
  5. await app.listen(3000); // 使用3000端口监听应用程序
  6. }
  7. bootstrap(); // 启动应用程序 -> localhost:3000

2.5 app.controller.spec.ts测试文件

修改了上述文件之后,命令行默认生成的测试文件也需要修改为异步的方式才能通过测试。

  1. import { Test, TestingModule } from '@nestjs/testing';
  2. import { AppController } from './app.controller';
  3. import { AppService } from './app.service';
  4. describe('AppController', () => {
  5. let appController: AppController;
  6. beforeEach(async () => {
  7. const app: TestingModule = await Test.createTestingModule({
  8. controllers: [AppController],
  9. providers: [AppService],
  10. }).compile();
  11. appController = app.get<AppController>(AppController);
  12. });
  13. describe('get', () => {
  14. it('should return "Hello World!"', async() => {
  15. const data=await appController.root();
  16. expect(data).toBe('Hello World!');
  17. });
  18. });
  19. });

3. 通用错误处理和接口文件

在项目实践中,注入错误处理、接口等模块化文件需要重复使用。最好集中在公用模块中。在项目根目录下创建common文件夹,并新建错误处理和接口模块。

3.1 common/errors.interceptor.ts错误处理文件

  1. import { CallHandler, ExecutionContext, HttpException, Injectable, NestInterceptor } from '@nestjs/common';
  2. import { Observable } from 'rxjs';
  3. import { catchError } from 'rxjs/operators';
  4. @Injectable()
  5. export class ErrorsInterceptor implements NestInterceptor {
  6. intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
  7. // 异常拦截器,拦截每个请求中的异常,目的是将异常码和异常信息改写为 { code: xxx, message: xxx } 类型
  8. return next.handle().pipe(catchError((error, caught): any => {
  9. if (error instanceof HttpException) {
  10. return Promise.resolve({
  11. code: error.getStatus(),
  12. message: error.getResponse()
  13. });
  14. }
  15. return Promise.resolve({
  16. code: 500,
  17. message: `出现了意外错误:${error.toString()}`
  18. });
  19. }));
  20. }
  21. }

3.2 common/result.interface.ts接口文件

  1. // 定义通用的API接口返回数据类型
  2. export interface Result {
  3. code: number;
  4. message: string;
  5. data?: any;
  6. }

4.创建数据库增删读写API文件

在根目录下新建cat文件夹,并依次创建下列文件。

4.1 cat.controller.ts

  1. import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common';
  2. import { Result } from 'common/result.interface';
  3. import { Cat } from './cat.entity';
  4. import { CatService } from './cat.service';
  5. @Controller('cat')
  6. export class CatController {
  7. constructor(
  8. @Inject(CatService) private readonly CatService: CatService,
  9. ) { }
  10. @Post()
  11. async createCat(@Body() Cat: Cat): Promise<Result> {
  12. await this.CatService.createCat(Cat);
  13. return { code: 200, message: '创建成功' };
  14. }
  15. @Delete(':id')
  16. async deleteCat(@Param('id') id: number): Promise<Result> {
  17. await this.CatService.deleteCat(id);
  18. return { code: 200, message: '删除成功' };
  19. }
  20. @Put(':id')
  21. async updateCat(@Param('id') id: number, @Body() Cat: Cat): Promise<Result> {
  22. await this.CatService.updateCat(id, Cat);
  23. return { code: 200, message: '更新成功' };
  24. }
  25. @Get(':id')
  26. async findOneCat(@Param('id') id: number): Promise<Result> {
  27. const data = await this.CatService.findOneCat(id);
  28. return { code: 200, message: '查询成功', data };
  29. }
  30. }

4.2 cat.service.ts文件

  1. import { HttpException, Injectable } from '@nestjs/common';
  2. import { InjectRepository } from '@nestjs/typeorm';
  3. import { Repository } from 'typeorm';
  4. import { Cat } from './cat.entity';
  5. @Injectable()
  6. export class CatService {
  7. constructor(
  8. @InjectRepository(Cat) private readonly catRepo: Repository<Cat>, // 使用泛型注入对应类型的存储库实例
  9. ) { }
  10. /**
  11. * 创建
  12. *
  13. * @param cat Cat 实体对象
  14. */
  15. async createCat(cat: Cat): Promise<Cat> {
  16. /**
  17. * 创建新的实体实例,并将此对象的所有实体属性复制到新实体中。 请注意,它仅复制实体模型中存在的属性。
  18. */
  19. // this.catRepo.create(cat);
  20. // 插入数据时,删除 id,以避免请求体内传入 id
  21. delete cat.id;
  22. return this.catRepo.save(cat);
  23. /**
  24. * 将给定实体插入数据库。与save方法不同,执行原始操作时不包括级联,关系和其他操作。
  25. * 执行快速有效的INSERT操作。不检查数据库中是否存在实体,因此如果插入重复实体,本次操作将失败。
  26. */
  27. // await this.catRepo.insert(cat);
  28. }
  29. /**
  30. * 删除
  31. *
  32. * @param id ID
  33. */
  34. async deleteCat(id: number): Promise<void> {
  35. await this.findOneById(id);
  36. this.catRepo.delete(id);
  37. }
  38. /**
  39. * 更新
  40. *
  41. * @param id ID
  42. * @param cat Cat 实体对象
  43. */
  44. async updateCat(id: number, cat: Cat): Promise<void> {
  45. await this.findOneById(id);
  46. // 更新数据时,删除 id,以避免请求体内传入 id
  47. delete cat.id;
  48. this.catRepo.update(id, cat);
  49. }
  50. /**
  51. * 根据ID查询
  52. *
  53. * @param id ID
  54. */
  55. async findOneCat(id: number): Promise<Cat> {
  56. return this.findOneById(id);
  57. }
  58. /**
  59. * 根据ID查询单个信息,如果不存在则抛出404异常
  60. * @param id ID
  61. */
  62. private async findOneById(id: number): Promise<Cat> {
  63. const catInfo = await this.catRepo.findOne(id);
  64. if (!catInfo) {
  65. throw new HttpException(`指定 id=${id} 的猫猫不存在`, 404);
  66. }
  67. return catInfo;
  68. }
  69. }

4.3 cat.entity.ts数据库实体文件

  1. import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
  2. @Entity('cat')
  3. export class Cat {
  4. /**
  5. * 自增主键
  6. */
  7. @PrimaryGeneratedColumn({
  8. comment: '自增ID'
  9. })
  10. id: number;
  11. /**
  12. * 昵称
  13. */
  14. @Column({
  15. comment: '昵称'
  16. })
  17. nickname: string;
  18. /**
  19. * 品种
  20. */
  21. @Column({
  22. comment: '品种'
  23. })
  24. species: string;
  25. }

4.4 cat.module.ts模块文件

  1. import { Module } from '@nestjs/common';
  2. import { TypeOrmModule } from '@nestjs/typeorm';
  3. import { CatController } from './cat.controller';
  4. import { Cat } from './cat.entity';
  5. import { CatService } from './cat.service';
  6. @Module({
  7. imports: [TypeOrmModule.forFeature([Cat])],
  8. controllers: [CatController],
  9. providers: [CatService],
  10. })
  11. export class CatModule { }

5.配置数据库

typeorm可以和各种数据库配合使用。为了方便调试,这里用sqlite3替换掉原案例教程中的Postgresql(否则还要装一个Postgresql数据库)。
在项目中安装sqlite3驱动。

  1. > npm i --save sqlite3

在根目录ormconfig.json文件中进行如下配置。注意,原案例中entities的目录在src/文件夹下面,实际使用时可能会报错(如果是直接运行TypeScript不会报错,如果编译成JavaScript就会报错,后面一种情况下需要修改目录到dist或者其他编译目标文件夹下)。

  1. {
  2. "type": "sqlite",
  3. "database": "./mydb.sql",
  4. "entities": [
  5. "dist/**/**.entity{.ts,.js}"
  6. ],
  7. "synchronize": true,
  8. "logging": true
  9. }

6.项目依赖包

项目中依赖的包文件如下,其中nestjs cli 已经安装好了一部分,typeorm等需要手动添加。

  1. "dependencies": {
  2. "@nestjs/common": "^6.7.2",
  3. "@nestjs/core": "^6.7.2",
  4. "@nestjs/platform-express": "^6.7.2",
  5. "@nestjs/typeorm": "^6.2.0",
  6. "mongodb": "^3.4.1",
  7. "pg": "^7.16.0",
  8. "reflect-metadata": "^0.1.13",
  9. "rimraf": "^3.0.0",
  10. "rxjs": "^6.5.3",
  11. "sqlite3": "^4.1.1",
  12. "typeorm": "^0.2.22"
  13. },
  14. "devDependencies": {
  15. "@nestjs/cli": "^6.9.0",
  16. "@nestjs/schematics": "^6.7.0",
  17. "@nestjs/testing": "^6.7.1",
  18. "@types/express": "^4.17.1",
  19. "@types/jest": "^24.0.18",
  20. "@types/node": "^12.7.5",
  21. "@types/supertest": "^2.0.8",
  22. "jest": "^24.9.0",
  23. "prettier": "^1.18.2",
  24. "supertest": "^4.0.2",
  25. "ts-jest": "^24.1.0",
  26. "ts-loader": "^6.1.1",
  27. "ts-node": "^8.4.1",
  28. "tsconfig-paths": "^3.9.0",
  29. "tslint": "^5.20.0",
  30. "typescript": "^3.7.4"
  31. }

7. 其他资源

-typeorm仓库
-Nestjs中文文档
-Jest测试框架中文文档
-github nest学习资源