看完这篇,Nestjs入门了

5,230 阅读22分钟

Nest.js 介绍

Nest (NestJS) 是一个用于构建高效、可扩展的 Node.js 服务器端应用的框架。它使用渐进式 JavaScript,构建并完全支持 TypeScript(但仍然允许开发者使用纯 JavaScript 进行编码)并结合了 OOP(面向对象编程)、FP(函数式编程)和 FRP(函数式反应式编程)的元素。

在底层,Nest 构建在强大的 HTTP 服务器框架上,例如 Express (默认),并且还可以通过配置从而使用 Fastify !也就是说可以使用 Express 的 API,或者 Fastify 的 API。

详情查看 nest.js 中文文档

为什么选择 Nest.js

  • 选择的原因:严格模块分类、上手容易、较多的学习资源,另外能用 Typescript 严格规范,nestjs 必定上榜。
  • 开发效率:论 reload 效率 nestjs 的热更新是增量更新,增量热更新快!
  • 个人原因:nestjs 使用 js 代码开发,又和 java 挺像,开发起来效率也高。

说了这么多,接下来开始吧!本文主要包含以下内容: image.png

创建一个 nest.js 项目

首先要确保你的操作系统上安装了 Node.js(版本 >= 16)。使用 Nest 命令行接口 设置新项目,只需输入下面命令即可。

npm i -g @nestjs/cli
nest new project-name

image-1.png

项目文件介绍

除去配置常见的配置文件之外,在 src 目录下有一些 NestJS 标准的文件规范:

src
 ├── app.controller.spec.ts
 ├── app.controller.ts
 ├── app.module.ts
 ├── app.service.ts
 └── main.ts
文件名文件描述
app.controller.spec.tsapp.controller 控制器的单元测试
app.controller.ts常见功能是用来处理 http 请求以及调用 service 层的处理方法,一般路由管理在这里
app.module.ts根模块用于处理其他类的引用与共享,一般以 module 文件夹来展示
app.service.ts封装通用的业务逻辑、与数据层的交互(例如数据库)、其他额外的一些三方请求
main.ts应用程序入口文件。它使用 NestFactory 用来创建 Nest 应用实例。

项目运行

安装过程完成后,通过查看 package.json 查看,输入以下命令即可启动应用:

npm run start

当然也可以运行下面命令启动,此命令将监视你的文件,会自动重新编译并重新加载服务器

npm run start:dev

启动服务后,首先找到入口文件 main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

从代码可以看出:首先使用 Nest.js 的工厂函数 NestFactory 来创建一个 AppModule 实例,再通过 app.listen 来监听 main.ts 中所定义的端口。

监听的端口号可以自定义,默认为3000。如果3000端口被占用,可以在main.ts里面修改默认端口号或创建env文件自定义端口号

在浏览器访问 http://localhost:3000 地址可以打开:

image-2.png

代码简析

main.ts 中可以看出,只有从 app.module 导出的 AppModule ,找到 app.module.ts 文件:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserController } from './user/user.controller';

@Module({
    imports: [],
    controllers: [AppController, UserController],
    providers: [AppService],
})
export class AppModule {}

AppModule 是应用程序的根模块,它提供了用来启动应用的引导机制,可以包含很多功能模块。

.module 文件需要使用一个 @Module() 装饰器的类,里面接收四个属性: providerscontrollersimportsexports

新增一个【用户模块】

首先运行下面命令,会快速自动生成一个用户的 UserController

nest g co user

不过此命令同时也会生成后缀为 spec 的测试文件。如果不需要每次都生成该文件,可以在根目录下的 nest-cli.json 添加如下配置,禁用测试用例生成。

"generateOptions": {
    "spec": false
}

此外,借助 CLI 的能力快速生成 CRUD 模块:

  • 生成一个模块(nest g mo)来组织代码,使其保持清晰的界限(Module)。
  • 生成一个控制器 (nest g co) 来定义 CRUD 路径(Controller)。
  • 生成一个服务 (nest g s) 来表示/隔离业务逻辑(Service)。
  • 生成一个实体类/接口来代表资源数据类型(Entity)。
  • 生成一个基础的 CRUD 功能,可以直接使用(nest g resource)

注意的是,使用命令创建 UserController 的同时,也会自动在 app.module.ts 里面注册好对应的 Controller。打开 UserController 文件,写下一个简单的 http 请求。

import { Controller, Get } from '@nestjs/common';

@Controller('user')
export class UserController {
    @Get()
    getHello(): string {
        return 'Hello Nest.js!';
    }
}

等待程序重新编译运行完毕之后,在浏览器输入 http://localhost:3000/user 访问即可看到:

image-3.png

Nest.js 控制器

控制器负责处理传入请求并向客户端返回响应。控制器的目的是接收应用的特定请求。路由机制控制哪个控制器接收哪些请求。通常,每个控制器都有不止一条路由,不同的路由可以执行不同的操作。

image-4.png

@Controller()

@Controller() 装饰器,这是定义基本控制器所必需的。该装饰器可以传入一个路径参数,作为访问这个控制器的主路径(不传则表示默认路径)

请求对象

处理程序通常要访问客户端请求的详细信息。通过将 @Req() 装饰器添加到处理程序的签名来指示 Nest 注入它来访问请求对象。

import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';

@Controller('user')
export class UserController {
    @Get()
    getHello(@Req() request: Request): string {
        console.log(request);
        return 'Hello Nest.js!';
    }
}

请求对象表示 HTTP 请求,并具有请求查询字符串、参数、HTTP 标头和正文的属性等。在大多数情况下,没有必要手动获取这些属性。我们可以使用开箱即用的专用装饰器,例如 @Body() 或 @Query()。下面是提供的装饰器列表和它们代表的普通平台特定对象。

@Request(), @Req()req
@Response(), @Res()*res
@Next()next
@Session()req.session
@Param(key?: string)req.params / req.params[key]
@Body(key?: string)req.body / req.body[key]
@Query(key?: string)req.query / req.query[key]
@Headers(name?: string)req.headers / req.headers[name]
@Ip()req.ip
@HostParam()req.hosts

资源

Nest 为所有标准的 HTTP 方法提供装饰器:@Get()、@Post()、@Put()、@Delete()、@Patch()、@Options() 和 @Head()。此外,@All() 定义了一个端点来处理所有这些。

import { Controller, Delete, Get, Patch, Post, Put } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
    constructor(private readonly appService: AppService) {}

    // 匹配到 get 请求 http://localhost:3000
    @Get()
    getHello(): string {
        return this.appService.getHello();
    }

    // 匹配到 get 请求 http://localhost:3000/list
    @Get('/list')
    getHelloList(): string {
        return '这是list页面';
    }

    // 匹配到 post 请求 http://localhost:3000/list
    @Post('/list')
    create(): string {
        return 'post请求页面';
    }

    // 匹配到 put 请求 http://localhost:3000/list
    @Put('/list')
    update(): string {
        return 'put请求页面';
    }

    // 匹配到 delete 请求 http://localhost:3000/list
    @Delete('/list')
    delete(): string {
        return 'delete请求页面';
    }

    // 匹配到 patch 请求 http://localhost:3000/list
    @Patch('/list')
    patch(): string {
        return 'patch请求页面';
    }
}

路由参数

当接受动态数据作为请求的一部分时(例如,GET /cats/1 获取 ID 为 1 的 cat),具有静态路径的路由将不起作用。为了定义带参数的路由,在路由的路径中添加路由参数标记,以捕获请求 URL 中该位置的动态值。使用 @Param() 装饰器访问以这种方式声明的路由参数,应将其添加到方法签名中。

@Get(':id')
findOne(@Param() params: any): string {
    console.log(params.id);
    return `This action returns a #${params.id} cat`;
}

请求负载

我们之前的 POST 路由处理程序不接受任何客户端参数。遇到这种问题怎么办呢?我们可以使用 @Body() 装饰器来解决。

但首先我们需要确定 DTO(数据传输对象)架构。DTO 是定义数据如何通过网络发送的对象。我们可以通过使用 TypeScript 接口或简单的类来确定 DTO 模式。下面我们来创建一个 CreateCatDto 类:

export class CreateCatDto {
    name: string;
    age: number;
    breed: string;
}

此后,我们可以在 appController 中使用新创建的 DTO

@Post()
async create(@Body() createCatDto: CreateCatDto) {
    return 'This action adds a new cat';
}

Nest.js 模块

模块是用 @Module() 装饰器注释的类。@Module() 装饰器提供 Nest 用于组织应用结构的元数据。

image-5.png 每个应用至少有一个模块,即根模块。根模块是 Nest 用于构建应用图的起点 - Nest 用于解析模块和提供器关系及依赖的内部数据结构。

@Module() 装饰器采用单个对象,其属性描述模块:

providers将由 Nest 注入器实例化并且至少可以在该模块中共享的提供程序
controllers此模块中定义的必须实例化的控制器集
imports导出此模块所需的提供程序的导入模块列表
exports这个模块提供的 providers 的子集应该在导入这个模块的其他模块中可用。你可以使用提供器本身或仅使用其令牌(provide 值)

输入 nest g resource user 生成一个基础的 CRUD 功能,现在目录结构为:

src
 ├── user
 |    ├── dto
 |    |   ├── create-user.dto.ts
 |    |   └── update-user.dto.ts
 |    |
 |    ├── entities
 |    |   └── user.entity.ts
 |    |
 |    ├── user.controller.spec.ts
 |    ├── user.controller.ts
 |    ├── user.module.ts
 |    └── user.service.ts
 |
 ├── app.module.ts
 └── main.ts

共享模块

在 Nest 中,默认情况下模块是单例,因此你可以轻松地在多个模块之间共享任何提供程序的同一实例。

image-6.png 每个模块自动成为共享模块。一旦创建,它就可以被任何模块重用。假设我们想要在其他几个模块之间共享 UserService 的一个实例。为此,我们首先需要将 UserService 提供程序添加到模块的 exports 数组中来导出它,如下所示:

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';

@Module({
    controllers: [UserController],
    providers: [UserService],
    exports: [UserService]
})
export class UserModule {}

现在,任何导入 UserModule 的模块都可以访问 UserService,并将与导入它的所有其他模块共享同一个实例。

模块重新导出

如上所示,模块可以导出其内部提供程序。此外,他们可以重新导出他们导入的模块。在下面的示例中,CommonModule 既被导入到 CoreModule 中,又被从 CoreModule 中导出,从而使其可用于导入该模块的其他模块。

@Module({
    imports: [CommonModule],
    exports: [CommonModule],
})
export class CoreModule {}

全局模块

如果你必须在所有地方导入相同的模块集,它会变得乏味。当你想要提供一组开箱即用的提供程序(例如辅助程序、数据库连接等)时,可以使用 @Global() 装饰器使模块全局化。

import { Module, Global } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';

@Global()
@Module({
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}

@Global() 装饰器使模块具有全局作用域。全局模块应该只注册一次,通常由根模块或核心模块注册。

动态模块

动态模块能够轻松创建可自定义的模块,这些模块可以动态注册和配置提供程序。如下示例:

import { Module, DynamicModule } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers';
import { Connection } from './connection.provider';

@Module({
    providers: [Connection],
    exports: [Connection],
})
export class DatabaseModule {
    static forRoot(entities = [], options?): DynamicModule {
        const providers = createDatabaseProviders(options, entities);
        return {
            module: DatabaseModule,
            providers: providers,
            exports: providers,
        };
    }
}

该模块默认定义 Connection 提供器(在 @Module() 装饰器元数据中),但另外 - 取决于传递到 forRoot() 方法中的 entitiesoptions 对象 - 公开提供器的集合,例如存储库。请注意,动态模块返回的属性扩展(而不是覆盖)@Module() 装饰器中定义的基本模块元数据。这就是从模块导出静态声明的 Connection 提供程序和动态生成的存储库提供程序的方式。

如果要在全局作用域内注册动态模块,请将 global 属性设置为 true

{
    global: true,
    module: DatabaseModule,
    providers: providers,
    exports: providers,
}

DatabaseModule 可以通过以下方式导入和配置。如果你想反过来重新导出一个动态模块,你可以省略 exports 数组中的 forRoot() 方法调用:

import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';

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

中间件、过滤器、管道、守卫、拦截器

中间件

中间件是在路由处理程序之前调用的函数。中间件函数可以访问 requestresponse 对象,以及应用请求-响应周期中的 next() 中间件函数。下一个中间件函数通常由名为 next 的变量表示。 image-5.png

中间件函数能够执行各种任务,如验证请求、解析请求体、处理错误等。具体而言,中间件函数可以用来:

  1. 执行请求预处理:对请求进行身份验证、数据解析、请求参数验证等操作,以确保请求的有效性和安全性。
  2. 在请求处理过程中执行特定任务:如记录日志、统计请求次数、缓存数据等。
  3. 处理错误:捕获和处理异常,返回适当的错误响应以提高应用程序的健壮性。
  4. 控制请求流程:中间件函数可以根据请求的特定属性或条件来决定是否将请求传递给下一个中间件或路由处理程序。

你可以在函数中或在具有 @Injectable() 装饰器的类中实现自定义 Nest 中间件。下面示例实现了一个简单的中间件函数:

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();
  }
}

应用中间件

我们使用模块类的 configure() 方法设置中间件。包含中间件的模块必须实现 NestModule 接口。

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
    imports: [CatsModule],
})
export class AppModule implements NestModule {
    configure(consumer: MiddlewareConsumer) {
        consumer
        .apply(LoggerMiddleware)
        .forRoutes('cats');
    }
}

此外,我们还可以通过在配置中间件时将包含路由 path 和请求 method 的对象传递给 forRoutes() 方法,进一步将中间件限制为特定的请求方法。

多个中间件

为了绑定顺序执行的多个中间件,只需在 apply() 方法中提供一个逗号分隔的列表:

consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);

全局中间件

使用 use() 方法即可:

const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);

异常过滤器

异常过滤器是负责处理应用中所有未处理的异常。当你的应用代码未处理异常时,该层会捕获该异常,然后自动发送适当的用户友好响应。

抛出标准异常

Nest 提供了一个内置的 HttpException 类,从 @nestjs/common 包中暴露出来。假如如下代码在这里因某种原因抛出异常。

@Get()
async findAll() {
    throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}

在客户端调用此接口的时候,响应如下展示:

{
    "statusCode": 403,
    "message": "Forbidden"
}

自定义异常

内置的 Nest HTTP 异常可能并不能达到想要的结果,这时候可以考虑自定义异常。通过这种方法,Nest 将识别你的异常,并自动处理错误响应。下面写一个简单的自定义异常:

export class ForbiddenException extends HttpException {
    constructor() {
        super('Forbidden', HttpStatus.FORBIDDEN);
    }
}

由于 ForbiddenException 扩展了基类 HttpException,它将与内置异常处理程序无缝协作。

管道

管道是用 @Injectable() 装饰器注释的类,它实现了 PipeTransform 接口。

管道有两个典型的用例:

  • 转型:将输入数据转换为所需的形式(例如,从字符串到整数)
  • 验证:评估输入数据,如果有效,只需将其原样传递;否则抛出异常

在这两种情况下,管道都在由 控制器路由处理器 处理的 arguments 上运行。Nest 在调用方法之前插入一个管道,管道接收指定给该方法的参数并对它们进行操作。任何转换或验证操作都会在此时发生,之后会使用任何(可能)转换的参数调用路由处理程序。

绑定管道

要使用管道,我们需要将管道类的实例绑定到适当的上下文。如果我们希望将管道与特定的路由处理程序方法相关联,并确保它在调用该方法之前运行。我们称为在方法参数级别绑定管道:

@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
    return this.catsService.findOne(id);
}

定制管道

每个管道都必须实现 transform() 方法来履行 PipeTransform 接口契约。这个方法有两个参数:

value 参数是当前处理的方法参数(在被路由处理方法接收之前),metadata 是当前处理的方法参数的元数据。元数据对象具有以下属性:

export interface ArgumentMetadata {
    type: 'body' | 'query' | 'param' | 'custom';
    metatype?: Type<unknown>;
    data?: string;
}

全局作用域管道

使用 useGlobalPipes 实现,使其应用于整个应用中的每个路由处理程序。

async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    app.useGlobalPipes(new ValidationPipe());
    await app.listen(3000);
}
bootstrap();

注意:在 混合应用 的情况下,useGlobalPipes() 方法不会为网关和微服务设置管道。对于 "standard"(非混合)微服务应用,useGlobalPipes() 会全局安装管道。

守卫

守卫是一个用 @Injectable() 装饰器注释的类,它实现了 CanActivate 接口。 Guards_1.png 守卫有单一的责任。它们根据运行时存在的某些条件(如权限、角色、ACL 等)确定给定请求是否将由路由处理程序处理。这通常称为授权。授权(及其通常与之合作的身份验证)通常由传统 Express 应用中的 中间件 处理。中间件是身份验证的不错选择,因为诸如令牌验证和将属性附加到 request 对象之类的事情与特定路由上下文(及其元数据)没有紧密联系。

守卫在所有中间件之后、任何拦截器或管道之前执行。

拦截器

拦截器是用 @Injectable() 装饰器注释并实现 NestInterceptor 接口的类。 Interceptors_1.png 每个拦截器都实现了 intercept() 方法,它有两个参数。第一个是 ExecutionContext 实例(与 guards 完全相同的对象)。第二个参数是 CallHandlerCallHandler 接口实现了 handle() 方法,你可以使用它在拦截器中的某个点调用路由处理程序方法。如果在 intercept() 方法的实现中不调用 handle() 方法,则根本不会执行路由处理程序方法。

试着写一个拦截器

第一步: 新建 src/common/exceptions/base.exception.filter.tshttp.exception.filter.ts 两个文件,从命名中可以看出它们分别处理统一异常与 HTTP 类型的接口相关异常。

base.exception.filter => Catch 的参数为空时,默认捕获所有异常

import { FastifyReply, FastifyRequest } from "fastify";

import {
    ExceptionFilter,
    Catch,
    ArgumentsHost,
    HttpStatus,
    ServiceUnavailableException,
    HttpException,
} from '@nestjs/common';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
    catch(exception: Error, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<FastifyReply>();
        const request = ctx.getRequest<FastifyRequest>();

        request.log.error(exception)
        
        // 非 HTTP 标准异常的处理。
        response.status(HttpStatus.SERVICE_UNAVAILABLE).send({
            statusCode: HttpStatus.SERVICE_UNAVAILABLE,
            timestamp: new Date().toISOString(),
            path: request.url,
            message: new ServiceUnavailableException().getResponse(),
        });
    }
}

http.exception.filter.ts => Catch 的参数为 HttpException 时, 将只捕获 HTTP 相关的异常错误

import { FastifyReply, FastifyRequest } from "fastify";
import {
    ExceptionFilter,
    Catch,
    ArgumentsHost,
    HttpException,
} from '@nestjs/common';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
    catch(exception: HttpException, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<FastifyReply>();
        const request = ctx.getRequest<FastifyRequest>();
        const status = exception.getStatus();

        response.status(status).send({
            statusCode: status,
            timestamp: new Date().toISOString(),
            path: request.url,
            message: exception.getResponse(),
        });
    }
}

第二步: 在 main.ts 文件中添加 useGlobalFilters 全局过滤器:

+ import { AllExceptionsFilter } from './common/exceptions/base.exception.filter';
+ import { HttpExceptionFilter } from './common/exceptions/http.exception.filter';
  // 异常过滤器
+ app.useGlobalFilters(new AllExceptionsFilter(), new HttpExceptionFilter());

除了全局异常拦截处理之外,我们需要再新建一个 business.exception.ts 来处理业务运行中预知且主动抛出的异常:

import { HttpException, HttpStatus } from '@nestjs/common';
import { BUSINESS_ERROR_CODE } from './business.error.codes';

type BusinessError = {
    code: number;
    message: string;
};

export class BusinessException extends HttpException {
    constructor(err: BusinessError | string) {
        if (typeof err === 'string') {
            err = {
                code: BUSINESS_ERROR_CODE.COMMON,
                message: err,
            };
        }
        super(err, HttpStatus.OK);
    }
    
    static throwForbidden() {
        throw new BusinessException({
            code: BUSINESS_ERROR_CODE.ACCESS_FORBIDDEN,
            message: '抱歉哦,您无此权限!',
        });
    }
}
export const BUSINESS_ERROR_CODE = {
    // 公共错误码
    COMMON: 10001,
    // 特殊错误码
    TOKEN_INVALID: 10002,
    // 禁止访问
    ACCESS_FORBIDDEN: 10003,
    // 权限已禁用
    PERMISSION_DISABLED: 10003,
    // 用户已冻结
    USER_DISABLED: 10004,
};

最后改造一下 HttpExceptionFilter,在处理 HTTP 异常返回之前先处理业务异常:

import { FastifyReply, FastifyRequest } from "fastify";
import {
    ExceptionFilter,
    Catch,
    ArgumentsHost,
    HttpException,
    HttpStatus,
} from '@nestjs/common';
import { BusinessException } from "./business.exception";

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
    catch(exception: HttpException, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<FastifyReply>();
        const request = ctx.getRequest<FastifyRequest>();
        const status = exception.getStatus();

        // 处理业务异常
        if (exception instanceof BusinessException) {
            const error = exception.getResponse();
            response.status(HttpStatus.OK).send({
                data: null,
                status: error['code'],
                extra: {},
                message: error['message'],
                success: false,
            });
            return;
        }

        response.status(status).send({
            statusCode: status,
            timestamp: new Date().toISOString(),
            path: request.url,
            message: exception.getResponse(),
        });
    }
}

MySQL操作

作为后端项目,必须得连接数据库。在这里,我们采用Mysql,毕竟相对来说Mysql应用广泛且非常稳定,有非常活跃的社区支持。

数据库安装

如果你电脑上还没有Mysql,那就去官网下载一个吧。Windows 上安装 MySQL 相对来说会较为简单, 就和安装一个应用程序差不多, 具体可以跟着Mysql的安装配置教程从零基础入门到精通,这里就不一一赘述了。

TypeORM

对象关系映射(Object Relational Mapping,简称 ORM)模式是一种为了解决面向对象与关系数据库存在的互不匹配现象的技术。

TypeORM 作为 Node.js 中老牌的 ORM 框架,无论是接口定义,还是代码实现方面都简单易懂、可读性高,也很容易对接多种数据源。TypeORM 使用 TypeScript 编写,在 NestJS 框架下运行得非常好,也是 NestJS 首推的 ORM 框架,有开箱即用的 @nestjs/typeorm 软件包支持。

前置条件:环境配置

NestJS 本身也自带了多环境配置方法。使用 @nestjs/config 会默认从项目根目录载入并解析一个 .env 文件。

  1. 安装依赖
npm i @nestjs/config -S

2. 安装完毕后,在 app.module.ts 中添加 ConfigModule 模块

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';
import { ConfigModule } from '@nestjs/config';

@Module({
    imports: [ConfigModule.forRoot(), UserModule],
    controllers: [AppController],
    providers: [AppService],
})

export class AppModule {}

NestJS 使用 TypeORM 的方式有两种。一种是 NestJS 提供的 @nestjs/typeorm 集成包,可以导出 TypeOrmModule.forRoot 方法来连接数据库。另外一种是直接使用 typeorm,自由封装 Providers 导入使用。在这里分别介绍一下:

使用 @nestjs/typeorm连接数据库

首先我们要安装依赖:

npm i @nestjs/typeorm typeorm mysql2 -S

其次我们在根目录下创建env文件用于存放数据库配置信息

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

然后在 app.module.ts 中连接数据库

import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigService, ConfigModule } from '@nestjs/config';
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';
import * as path from 'path';

const envPath = path.resolve('.env');

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: [envPath],
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => ({
        type: 'mysql', // 数据库类型
        entities: [],  // 数据表实体
        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, //根据实体自动创建数据库表, 生产环境建议关闭
      }),
    }),
    UserModule
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

直接使用 typeorm,自由封装 Providers 

首先我们要安装依赖:

npm i typeorm mysql2 -S
npm i dotenv --save-dev

其次我们在根目录下创建env文件用于存放数据库配置信息

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

创建 database.providers.ts文件:

import { DataSource, DataSourceOptions } from 'typeorm';
import * as dotenv from 'dotenv';

// 设置数据库类型
const databaseType: DataSourceOptions['type'] = 'mysql';
const envConfig = dotenv.config().parsed;

const DATA_CONFIG = {
    ...envConfig,
    type: databaseType,
    entities: [],
};

const MYSQL_DATA_SOURCE = new DataSource(DATABASE_CONFIG);

// 数据库注入
export const DatabaseProviders = [
    {
        provide: 'MYSQL_DATA_SOURCE',
        useFactory: async () => {
            await MYSQL_DATA_SOURCE.initialize()
            return MYSQL_DATA_SOURCE
        }
    }
];

创建 database.module.ts 文件

import { Module } from '@nestjs/common';
import { DatabaseProviders } from './database.providers';

@Module({
    providers: [...DatabaseProviders],
    exports: [...DatabaseProviders],
});

export class DatabaseModule {}

CRUD

通过typeORM已经连接好了数据库,那么我们现在要创建数据表,并通过接口实现CRUD功能。

创建 user.entity.ts 实例

import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";

@Entity('user')
export class User {
    @PrimaryGeneratedColumn()
    id: number; // 标记为主列,值自动生成
    
    @Column({ default: null })
    name: string;
    
    @Column({type: 'timestamp', default: () => "CURRENT_TIMESTAMP"})
    create_time: Date;
    
    @Column({type: 'timestamp', default: () => "CURRENT_TIMESTAMP"})
    update_time: Date;
}

修改 user.service.ts 文件,设置 CRUD

import { HttpException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';

@Injectable()
export class UserService {
    constructor(
        @InjectRepository(User)
        private readonly userRepository: Repository<User>,
    ) {}

    async create(createUserDto: Partial<User>): Promise<User> {
        const { name } = createUserDto;
        if (!name) {
            throw new HttpException('名字不能为空', 401);
        }
        const userInfo = await this.userRepository.findOne({ where: { name } });
        if (userInfo) {
            throw new HttpException('名称不能重复', 401);
        }
        return await this.userRepository.create(createUserDto);
    }

    async findById(id): Promise<User> {
        return await this.userRepository.findOne(id);
    }

    async updateById(id, user): Promise<User> {
        const existUser = await this.userRepository.findOne(id);
        if (!existUser) {
            throw new HttpException('用户不存在', 401);
        }
        const updateUser = this.userRepository.merge(existUser, user);
        return this.userRepository.save(updateUser);
    }

    async remove(id) {
        const existUser = await this.userRepository.findOne(id);
        if (!existUser) {
            throw new HttpException('用户不存在', 401);
        }
        return await this.userRepository.remove(existUser);
    }
}

user.module.ts 加上相关引入

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';

@Module({
    imports: [TypeOrmModule.forFeature([User])],
    controllers: [UserController],
    providers: [UserService],
})
export class UserModule {}

user.controller.ts 加上相关路由

import {
    Controller,
    Get,
    Post,
    Body,
    Patch,
    Param,
    Delete,
    Put,
} from '@nestjs/common';
import { UserService } from './user.service';
@Controller('user')
export class UserController {
    constructor(private readonly userService: UserService) {}

    @Post()
    async create(@Body() createUserDto) {
        return await this.userService.create(createUserDto);
    }

    @Get(':id')
    async findById(@Param('id') id: string) {
        return await this.userService.findById(id);
    }

    @Put(':id')
    async update(@Param('id') id: string, @Body() updateUserDto) {
        return await this.userService.updateById(id, updateUserDto);
    }

    @Delete(':id')
    async remove(@Param('id') id: string) {
        return await this.userService.remove(id);
    }
}

现在基本上就可以通过 postman 来调用上面的接口了。

注意:本文使用上面所写的第一种方式连接数据库。在创建完 entity 后,要将对应的 entity 加到 app.module.ts 中。

接口文档

最后,作为一个后端接口,接口文档必不可少。一般接口文档都采用swagger。用它的原因一方面是 Nest.js提供了专用的模块来使用它,其次可以精确的展示每个字段意义,只要注解写的到位!

安装依赖

npm i @nestjs/swagger -S

创建 doc.ts 文件

import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import * as packageConfig from '../package.json'

export const generateDocument = (app) => {
  const options = new DocumentBuilder()
    .setTitle(packageConfig.name)
    .setDescription(packageConfig.description)
    .setVersion(packageConfig.version)
    .build();

  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('/api/doc', app, document);
}

为了方便,swagger 的配置信息都从 package.json 中获取,当然你也可以自己维护配置信息的文件。

默认情况下,ts项目中不能直接导入 .json 的模块,所以需要在 tsconfig.json 中新增 resolveJsonModule: true 配置。

main.ts 中引入方法即可

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { generateDocument } from './doc';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  generateDocument(app);
 
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

通过上面操作后,启动服务,在浏览器输入 http://localhost:3000/api/doc 就可以看到了。

捕获.PNG

接口标签

我们在对应的 controller 文件中加入 @ApiTags 就可以了

import {
    Controller,
    Get,
    Post,
    Body,
    Patch,
    Param,
    Delete,
    Put,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { UserService } from './user.service';

@ApiTags('用户')
@Controller('user')
export class UserController {
...

捕获.PNG

总结

结合官方文档以及网上查阅的各种资料,总算将基本用到的东西抽取总结了。这只是开始的第一步,后面将还会有更多复杂场景,继续努力吧,加油!