使用 NestJS 和 Prisma 创建 REST API

3,803 阅读17分钟

使用 NestJS 和 Prisma 创建 REST API

原文标题:Building a REST API with NestJS and Prisma

NestJS 是著名的 Node.js 框架之一,获得了越来越多的开发者的喜爱和关注。本文将教你如何使用NestJS、Prisms、Postgres 和 Swagger 来构建后端 REST API。

1 (1).png

简介

在本教程中,你将学会如何为一个名为“Median”(简单版 Medium)的博客应用构建后端 REST API。你将从创建一个新的 NestJS 项目开始。然后你将启动自己的 PostgreSQL 服务并使用 Prisma 连接它。最后,你将构建 REST API 并用 Swagger 来创建接口文档。

final-app (1).png

你将使用到的技术

你将使用以下工具来构建这个应用:

  • NestJS 作为后端框架
  • Prisma 作为对象关系映射器(ORM)
  • POstgreSQL 作为数据库
  • Swagger 作为 API 文档工具
  • TypeScript 作为编程语言

先决条件

假定的知识储备

虽然这是一篇对新手友好的教程,但是,这篇教程依然假定你已具备:

  • JavaScript 或 TypeScript 知识(更佳)的基础知识
  • NestJS 的基础知识

注意:如果你对 NestJS 不熟悉,你可以通过 NestJS 文档中的快速上手一章来快速学习它的基础知识。

开发环境

按照提供的示例进行操作,你需要:

  • 安装 Nede.js。
  • 安装 Docker 和 PostgreSQL。
  • 安装 Prisma VSCode 扩展插件。(可选)
  • 可以访问 Unix shell(如 Linux 和 macOS 中的终端/命令行)来运行本系列中提供的命令。(可选)

提示1:可选的 Prisma VSCode 扩展插 Prisma 添加了一些非常棒的智能感知和语法高亮功能。

提示2:如果你没有 Unix shell(比如,你在 Windows 机器上开发),你仍可以参照这些,但是需要将命令修改为适应你机器的。

生成 NestJS 项目

首先你需要安装 NestJS CLI 命令行工具。在处理 NestJS 项目时,NestJS CLI 工具非常方便。它内置的实用程序,可以帮你初始化、开发和维护你的 NestJS 应用。

你可以使用 NestJS CLI 来创建一个空的项目。开始,在你想要创建项目的目录运行以下命令:

npx @nestjs/cli new median

CLI 工具将提示你为你的项目选择一个包管理工具 —— 选择 npm。然后,在当前目录下你应该已经有了一个新的 NestJS 项目。

用你首选的代码编辑器(我们推荐 VSCode)打开这个项目。你应该看到以下文件:

median
  ├── node_modules
  ├── src
  │   ├── app.controller.spec.ts
  │   ├── app.controller.ts
  │   ├── app.module.ts
  │   ├── app.service.ts
  │   └── main.ts
  ├── test
  │   ├── app.e2e-spec.ts
  │   └── jest-e2e.json
  ├── README.md
  ├── nest-cli.json
  ├── package-lock.json
  ├── package.json
  ├── tsconfig.build.json
  └── tsconfig.json

大部分你需要编写的代码都放在 src 目录。NestJS CLI 已经帮你创建了一些文件。一些注意点:

  • src/app.module.ts:是应用的根模块。
  • src/app.controller.ts:只有一个路由的基础控制器:/ 。这个路由返回了一个简单的 “Hello World!” 信息。
  • src/main.ts:应用的入口文件。它将启动 NestJS 应用。

你可以使用以下命令来启动你的项目:

npm run start:dev

这个命令将监控你的文件,当你改动代码时会自动重编译并重新加载服务。为验证服务是否已经运行,可以打开 URL:http://localhost:3030/ 。你应该可以看到一个只有 “Hello world!” 信息的空页面。

提示:当你阅读本教程时你最好保持这个服务一直在后台运行。

创建一个 PostgreSQL 实例

你将使用 PostgreSQL 作为 NestJS 应用的数据库。本教程会教你如何在你本机的 Docker 容器上安装并运行 PostgreSQL。

提示:如果你不想使用 Docker,你可以在本地安装一个 PostgreSQL 实例或者在 Heroku 上面获得一台部署好 PostgreSQL 数据库。

首先,在你的项目主目录中创建一个 docker-compose.yml 文件:

touch docker-compose.yml

这个 docker-compose.yml 文件是一个配置文件,它包含了运行带有 PostgreSQL 设置的 docker 容的规范。在文件中创建以下配置:

# docker-compose.yml

version: '3.8'
services:

  # Docker connection string: postgres://postgres:postgres@localhost:5432/

  postgres:
    image: postgres:13.5
    restart: always
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
    volumes:
      - postgres:/var/lib/postgresql/data
    ports:
      - '5432:5432'

volumes:
  postgres:

关于此配置需要了解的一些事项:

  • image 选项定义了要使用的 Docker 镜像。在这里,你可以使用这个13.5版的 postgres 镜像。
  • environment 选项声明了在初始化期间传入的环境变量。你可以定义配置项和密码 - 比如用户名和密码 - 容器会在这里使用。
  • volumes 选项用于在主机文件系统中持久化数据。
  • ports 选项映射从宿主机到容器的端口。其格式遵循 ‘host_port:container_port’ 的约定。在本例中,你将映射宿主机的 5432 端口到 postgres 容器的 5432 端口。PostgreSQL 通常使用的端口就是 5432

确保你本机的 5432 端口没有被其他应用占用。要启动 postgres 容器,请打开一个新的终端窗口并在项目的主目录下运行以下命令:

docker-compose up

如果一切顺利,新的终端窗口应该出现数据库已准备接受连接的日志了。你应该在终端窗口中看到类似以下的日志:

...
postgres_1  | 2022-03-05 12:47:02.410 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
postgres_1  | 2022-03-05 12:47:02.410 UTC [1] LOG:  listening on IPv6 address "::", port 5432
postgres_1  | 2022-03-05 12:47:02.411 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
postgres_1  | 2022-03-05 12:47:02.419 UTC [1] LOG:  database system is ready to accept connections

恭喜🎉。现在你已经有了自己的 PostgreSQL 数据库。

提示:如果你关闭了终端窗口,容器也将停止。你可以通过在命令最后添加一个 -d 选项来避免出现这种情况,就像这样:docker-compose up -d。这样就会无限期地在后台运行容器。

Prisma 设置

现在数据库已经准备好了,是时候来设置 Prisma 了!

初始化 Prisma

首先,安装 Prisma CLI 作为开发依赖项。Prisma CLI 将允许你运行各种命令并和你的项目进行交互。

npm install -D prisma

你可以通过运行以下命令来初始化你的项目:

npx prisma init

这将创建一个新的 prisma 文件夹,里面包含一个 schema.prisma 文件。这个就是包含你数据库架构的主要配置文件。此命令同时也会在你的项目内创建一个 .env 文件。

设置环境变量

在 .env 文件里,你应该看到一个带有虚假连接字符串的 DATABASE_URL 环境变量。使用你自己的 PostregreSQL 实例来替换这个连接字符串。

// .env
DATABASE_URL="postgres://postgres:postgres@localhost:5432/median-db"

提示:如果你不用 docker(如上一张所述)来创建你的 PostgreSQL 数据库,你的连接字符串将和以上所示的不同。你可以在 Prisma Docs 中找到正确的 PostgreSQL 连接字符串格式。

理解 Prisma schema

当你打开 prisma/schema.prisma,你将看到以下的默认 schema 内容:

// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

该文件使用 Prisma Schema Language 书写,这是 Prisma 用来定义数据库模式的一种语言。schema.prisma 文件主要包含三个组件:

  • 数据源:定义你的数据库连接。上面配置表示你的数据库提供者是 PostgreSQL,并且数据库连接字符串存在 DATABASE_URL 环境变量中。
  • 生成器:表示你要生成 Prisma Client,这是一个类型安全的数据库查询构建器。它用于向你的数据库发送查询语句。
  • 数据模型:定义你的数据库模型。每一个模型都将被映射为底层数据库中的一张表。现在你的 schema 中没有任何模型,你将在下一章节中探索这部分内容。

提示:要了解更多关于 Prisma schema 的信息,可以查看 Prisma docs

数据建模

现在是时候为你的应用定义数据模型了。对于本教程,你只需要一个 Article 模型来表示博客上的每篇文章。

在 prisma/prisma.schema 文件里,为你的 schema 添加一个名为 Article 的新模型:

// prisma/schema.prisma

model Article {
  id          Int      @id @default(autoincrement())
  title       String   @unique
  description String?
  body        String
  published   Boolean  @default(false)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
}

这里,你已经创建了一个包含几个字段的 Article 模型。每个字段都有一个名称(idtitle 等),一个类型(IntString 等),还有其他可选的属性(@id@unique 等)。你可以通过在字段类型后添加一个 ? 来使该字段变得可选。

id 字段有一个特定的属性叫 @id。这个属性表示该字段是模型的主键。@default(autoincrement()) 属性表示该字段应自动递增并分配给任何新创建的记录。

published 字段是一个标记,用于表示文章是已发布还是处于草稿状态。@default(false) 属性表示该字段默认会被设置为 false

那两个 DateTime 字段,createdAtupdatedAt,将跟踪文章的创建时间和最近一次更新时间。当文章被修改时, @updatedAt 属性将使用当前时间戳自动更新该字段。每当文章修改时就会使用当前时间戳为该字段添加日期。

迁移数据库

定义 Prisma schema 后,你将运行迁移脚本以在数据库中创建实体表。要生成并执行您的第一次迁移,请在终端中运行以下命令:

npx	prisma migrate dev --name "init"

该命令将会做三件事:

  1. 保存迁移:Prisma Migrate 将对你的 schema 创建快照,并生成执行迁移所需的 SQL 命令。Prisma 会将包含 SQL 命令的迁移文件保存到一个新建的 prisma/migrations 文件夹内。
  2. 执行迁移:Prisma Migrate 将执行迁移文件中的 SQL 命令以在数据库中创建底层数据表。
  3. 生成 Prisma Client:Prisma 将会基于你最新的 schema 文件生成 Prisma Client。如果你本地没有安装 Client 库文件,CLI 会自动为你安装。你会在 package.json 文件的 dependencies 中看到 @prisma/client 包。Prisma Client 是一个基于你的 Prisma schema 自动生成的 TypeScript 查询构建器。

提示:你可以在 Prisma docs 文档中学习更多 Prisma Migrate 的知识。

如果你完全成功了,你会看到以下信息:

The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20220528101323_init/
    └─ migration.sql

Your database is now in sync with your schema.
...
✔ Generated Prisma Client (3.14.0 | library) to ./node_modules/@prisma/client in 31ms

看一下生成的迁移文件,你就能了解 Prisma Migrate 在幕后都做了什么。

-- prisma/migrations/20220528101323_init/migration.sql

-- CreateTable
CREATE TABLE "Article" (
    "id" SERIAL NOT NULL,
    "title" TEXT NOT NULL,
    "description" TEXT,
    "body" TEXT NOT NULL,
    "published" BOOLEAN NOT NULL DEFAULT false,
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updatedAt" TIMESTAMP(3) NOT NULL,

    CONSTRAINT "Article_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "Article_title_key" ON "Article"("title");

注意:最终生成的迁移文件名会略有不同。

这是一段在你的 PostgreSQL 数据库中创建 Article 表时所需要的 SQL 命令。它是由 Prisma 基于你的 Prisma schema 自动生成和执行的。

数据填充

目前为止,数据库还是空的。所以你需要创建一个种子脚本,该脚本将使用一些虚拟数据填充数据库。

首先,创建一个名为 prisma/seed.ts 的种子文件。该文件将包含填充数据库所需要的虚拟数据和查询语句。

touch prisma/seed.ts

然后,在种子文件内,添加以下代码:

// prisma/seed.ts

import { PrismaClient } from '@prisma/client';

// initialize Prisma Client
const prisma = new PrismaClient();

async function main() {
  // create two dummy articles
  const post1 = await prisma.article.upsert({
    where: { title: 'Prisma Adds Support for MongoDB' },
    update: {},
    create: {
      title: 'Prisma Adds Support for MongoDB',
      body: 'Support for MongoDB has been one of the most requested features since the initial release of...',
      description:
        "We are excited to share that today's Prisma ORM release adds stable support for MongoDB!",
      published: false,
    },
  });

  const post2 = await prisma.article.upsert({
    where: { title: "What's new in Prisma? (Q1/22)" },
    update: {},
    create: {
      title: "What's new in Prisma? (Q1/22)",
      body: 'Our engineers have been working hard, issuing new releases with many improvements...',
      description:
        'Learn about everything in the Prisma ecosystem and community from January to March 2022.',
      published: true,
    },
  });

  console.log({ post1, post2 });
}

// execute the main function
main()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  })
  .finally(async () => {
    // close Prisma Client at the end
    await prisma.$disconnect();
  });

在这个脚本里,你将首先初始化 Prisma Client。然后通过 prisma.upsert() 方法创建两篇文章。upsert 方法当且仅当 where 条件匹配不到任何文章时才会创建新的文章。你正在使用 upsert 查询替代 create 查询,因为 upsert 删除了与意外尝试重复插入相同记录有关的错误。

你需要告诉 Prisma,当运行种子命令时要执行哪个脚本。你可以通过在 package.json 文件的最后添加 prisma.seed 这个健来做到这点:

// package.json

// ...
  "scripts": {
    // ...
  },
  "dependencies": {
    // ...
  },
  "devDependencies": {
    // ...
  },
  "jest": {
    // ...
  },
+  "prisma": {
+    "seed": "ts-node prisma/seed.ts"
+  }

seed 命令会执行你之前定义好的 prisma/seed.ts 脚本。这个命令应该会自动运行,因为 ts-node 已经作为开发依赖被安装在 package.json 中。

使用以下命令来执行填充:

npx prisma db seed

你会看到以下输出:

Running seed command `ts-node prisma/seed.ts` ...
{
  post1: {
    id: 1,
    title: 'Prisma Adds Support for MongoDB',
    description: "We are excited to share that today's Prisma ORM release adds stable support for MongoDB!",
    body: 'Support for MongoDB has been one of the most requested features since the initial release of...',
    published: false,
    createdAt: 2022-04-24T14:20:27.674Z,
    updatedAt: 2022-04-24T14:20:27.674Z
  },
  post2: {
    id: 2,
    title: "What's new in Prisma? (Q1/22)",
    description: 'Learn about everything in the Prisma ecosystem and community from January to March 2022.',
    body: 'Our engineers have been working hard, issuing new releases with many improvements...',
    published: true,
    createdAt: 2022-04-24T14:20:27.705Z,
    updatedAt: 2022-04-24T14:20:27.705Z
  }
}

🌱  The seed command has been executed.

注意:你可以在 Prisma Docs 文档中了解更多关于填充的信息。

创建 Prisma 服务

在 NestJS 应用中,从你的应用程序中抽象出 Prisma Client API 是一种很好的做法。为此,你将创建一个包含 Prisma Client 的新的服务。这个服务被叫做 PrismaService,它将负责实例化 Prisma Client 实例并连接你的数据库。

Nest CLI 给你提供了一个简单的方法,可以在命令行中直接创建模块和服务。在你的终端中运行以下命令:

npx nest generate module prisma
npx nest generate service prisma

提示1:如果需要,可以参考 NestJS 官方文档中关于服务模块的介绍。

提示2:在某些情况下,当服务器已经运行时执行 nest generate 命令,可能会导致 NestJS 抛出异常:Error: Cannot find module './app.controller’。如果你遇到这个错误,需要在终端中运行以下命令:rm -rf dist 并且重启服务器。

这将生成一个包含 prisma.module.tsprisma.service.ts 的新的子文件夹 .src/prisma。使用以下代码更新服务文件:

// src/prisma/prisma.service.ts

import { INestApplication, Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient {
  async enableShutdownHooks(app: INestApplication) {
    this.$on('beforeExit', async () => {
      await app.close();
    });
  }
}

为确保你的应用程序能够优雅地关闭,enableShutdownHooks 的定义是必要的。NestJS 文档中提供了更多信息。

Prisma 模块将负责创建 PrismaService 的单例实例,并允许在整个应用中共享服务。要做到这点,你需要把 PrismaService 添加到 prisma.module.ts 文件的 exports 数组中:

// src/prisma/prisma.module.ts

import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

现在,任意一个导入 PrismaModule 的模块都可以访问 PrismaService 并可以把它注入到自己的组件或服务中。这是 NestJS 应用的常见模式。

有了这些,你就完成了 Prisma 的设置!你现在可以着手去构建 REST API了。

Swagger 设置

Swagger 是一个使用 OpenAPI 规范记录 API 的工具。Nest 有一个专门用于 Swagger 的模块,你很快就会用到它。

通过安装所需的依赖项开始:

npm install --save @nestjs/swagger swagger-ui-express

现在打开 main.ts,通过 SwaggerModule 类来初始化 Swagger

// src/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

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

  const config = new DocumentBuilder()
    .setTitle('Median')
    .setDescription('The Median API description')
    .setVersion('0.1')
    .build();

  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);

  await app.listen(3000);
}
bootstrap();

在应用程序运行时,打开浏览器并导航到 http://localhost:3000/api。你应该会看到 Swagger UI。

swagger-ui (1).png

Article 模型实现 CRUD 操作

在本节中,你将为 Article 模型和其附带的业务逻辑实现创建、读取、更新和删除(CRUD)操作。

生成 REST 资源

在实现 REST API 之前,你还需要为 Article 模型生成 REST 资源。使用 Nest CLI 可以快速完成。在终端中运行以下命令:

npx nest generate resource

你将收到一些 CLI 提示。相应地回答问题:

  1. What name would you like to use for this resource (plural, e.g., "users")? articles
  2. What transport layer do you use? REST API
  3. Would you like to generate CRUD entry points? Yes

你现在应该发现了一个新的 src/articles 目录,其中包含有 REST 端点的所有模版。在 src/articles/articles.controller.ts 文件中,你会看到不能路由的定义(也叫做路由处理器)。处理每一个请求的业务逻辑被封装在 src/articles/articles.service.ts 文件中。目前,该文件也包含了虚拟数据的实现。

如果你再次打开 Swagger API 页面,你会看到以下所示内容:

articles-crud-1 (1).png

SwaggerModule 搜索了路由处理器中的所有 @Body()@Query(),和 @Param() 装饰器,来生成这个 API 页面。

添加 PrismaClientArticles 模块

为了能在 Articles 模块中访问 PrismaClient,你必须把 PrismaModule 作为导入项添加进来。将以下导入项添加到 ArticlesModule

// src/articles/articles.module.ts

import { Module } from '@nestjs/common';
import { ArticlesService } from './articles.service';
import { ArticlesController } from './articles.controller';
import { PrismaModule } from 'src/prisma/prisma.module';

@Module({
  controllers: [ArticlesController],
  providers: [ArticlesService],
  imports: [PrismaModule],
})
export class ArticlesModule {}

现在你可以在 ArticlesService 中注入 PrismaService 并使用它来访问数据库。为此,在 articles.service.ts 中添加一个构造器,如下所示:

// src/articles/articles.service.ts

import { Injectable } from '@nestjs/common';
import { CreateArticleDto } from './dto/create-article.dto';
import { UpdateArticleDto } from './dto/update-article.dto';
import { PrismaService } from 'src/prisma/prisma.service';

@Injectable()
export class ArticlesService {
  constructor(private prisma: PrismaService) {}

  // CRUD operations
}

定义 GET /articles 端点

该端点的控制器被叫做 findAll。该端点将返回数据库中所有已发布的文章。findAll 控制器如下所示:

// src/articles/articles.controller.ts

@Get()
findAll() {
  return this.articlesService.findAll();
}

你需要更新 ArticlesService.findAll(),返回一个数据库中所有已发布文章的数组:

// src/articles/articles.service.ts

@Injectable()
export class ArticlesService {
  constructor(private prisma: PrismaService) {}

  create(createArticleDto: CreateArticleDto) {
    return 'This action adds a new article';
  }

  findAll() {
-    return `This action returns all articles`;
+    return this.prisma.article.findMany({ where: { published: true } });
  }

findMany 查询将返回符合 where 条件的所有 article 记录。

你可以通过访问 http://localhost:3000/api 并点开 GET/articles 下拉按钮来测试端点。按 Try it out,然后按 Execute 来查看结果。

swagger-findall (1).png

注意:你也可以直接在浏览器或者通过一个 REST 客户端(像 Postman)来运行所有的请求。Swagger也同样为每个请求生成了 curl 命令,以便你在终端中运行 HTTP 请求。

定义 GET /articles/drafts 端点

你将定义一个新的路由来获取所有未发布的文章。NestJS 没有为这个端点自动生成路由控制器,所以你需要自己写:

// src/articles/articles.controller.ts

@Controller('articles')
export class ArticlesController {
  constructor(private readonly articlesService: ArticlesService) {}

  @Post()
  create(@Body() createArticleDto: CreateArticleDto) {
    return this.articlesService.create(createArticleDto);
  }

+  @Get('drafts')
+  findDrafts() {
+    return this.articlesService.findDrafts();
+  }

  // ...

}

你的编辑器应该会报错,因为 articlesService.findDrafts() 函数不存在。因此,需要在 ArticlesService 中实现 findDrafts 这个函数:

// src/articles/articles.service.ts

@Injectable()
export class ArticlesService {
  constructor(private prisma: PrismaService) {}

  create(createArticleDto: CreateArticleDto) {
    return 'This action adds a new article';
  }

+  findDrafts() {
+    return this.prisma.article.findMany({ where: { published: false } });
+  }

  // ...

}

现在 GET /articles/drafts 端点在 Swagger API 页面中可用了。

注意:我建议在完成实现后通过 Swagger API 页面测试每个端点。

定义 GET /articles/:id 端点

这个端点的路由控制器叫做 findOne。如下所示:

// src/articles/articles.controller.ts

@Get(':id')
findOne(@Param('id') id: string) {
  return this.articlesService.findOne(+id);
}

该路由接受一个动态的参数 id,来传递给 findOne 路由控制器。由于 Articles 模型具有整数 id 字段,因此需要使用需要使用 + 运算符将 id 参数转换成数字。

现在,更新 ArticlesService 中的 findOne 方法来返回具有给定 id 的文章:

// src/articles/articles.service.ts
@Injectable()
export class ArticlesService {
  constructor(private prisma: PrismaService) {}

  create(createArticleDto: CreateArticleDto) {
    return 'This action adds a new article';
  }

  findAll() {
    return this.prisma.article.findMany({ where: { published: true } });
  }

  findOne(id: number) {
-    return `This action returns a #${id} article`;
+    return this.prisma.article.findUnique({ where: { id } });
  }
}

再次,通过访问 http://localhost:3000/api 来测试这个端点。点击 GET /articles/{id} 下来按钮。按 Try it out,给 id 参数添加一个有效值,并按 Execute 来查看结果。

swagger-findone (1).png

定义 POST /articles 端点

该端点是为了创建新文章。该端点的路由处理器叫做 create。如下所示:

// src/articles/articles.controller.ts

@Post()
create(@Body() createArticleDto: CreateArticleDto) {
  return this.articlesService.create(createArticleDto);
}

请注意,它需要请求体中类型为 CreateArticleDto 的参数。DTO(数据传输对象)是一个定义数据如何在网络中传输的对象。目前,CreateArticleDto 是一个空类。你将向其添加属性以定义请求体的规范。

// src/articles/dto/create-article.dto.ts

import { ApiProperty } from '@nestjs/swagger';

export class CreateArticleDto {
  @ApiProperty()
  title: string;

  @ApiProperty({ required: false })
  description?: string;

  @ApiProperty()
  body: string;

  @ApiProperty({ required: false, default: false })
  published?: boolean = false;
}

@ApiProperty 装饰器是必须的,它使得类属性对 SwaggerModule 可见。更多信息请查阅 NestJS 文档

CreateArticleDto 现在应该在 Swagger API 页面中的 Schemas 下定义了。UpdateArticleDto 的规范是从 CreateArticleDto 中自动推断出来的。所以 UpdateArticleDto 也已经被 Swagger 定义好了。

create-article-dto (1).png

现在更新 ArticlesService 中的 create 方法在数据库中创建一篇新文章。

// src/articles/articles.service.ts

@Injectable()
export class ArticlesService {
  constructor(private prisma: PrismaService) {
  }

  create(createArticleDto: CreateArticleDto) {
-    return 'This action adds a new article';
+    return this.prisma.article.create({ data: createArticleDto });
  }

  // ...
}

定义 PATCH /articles/:id 端点

该端点是用来更新已存在的文章。该端点的路由处理器名字叫 update。如下所示:

// src/articles/articles.controller.ts

@Patch(':id')
update(@Param('id') id: string, @Body() updateArticleDto: UpdateArticleDto) {
  return this.articlesService.update(+id, updateArticleDto);
}

UpdateArticleDto 被定义为 CreateArticleDtoPartialType。所以它也具有 CreateArticleDto 的所有属性。

// src/articles/dto/update-article.dto.ts

import { PartialType } from '@nestjs/swagger';
import { CreateArticleDto } from './create-article.dto';

export class UpdateArticleDto extends PartialType(CreateArticleDto) {}

正如之前一样,你必须为该操作更新相应的服务方法:

// src/articles/articles.service.ts

@Injectable()
export class ArticlesService {
  constructor(private prisma: PrismaService) {}

  // ...

  update(id: number, updateArticleDto: UpdateArticleDto) {
-    return `This action updates a #${id} article`;
+    return this.prisma.article.update({
+      where: { id },
+      data: updateArticleDto,
+    });
  }

  // ...
}

article.update 操作将会查找具有给定 idArticle 记录,并使用 UpdateArticleDto 的数据来更新它。

如果在数据库中未找到这个 Article 记录,Prisma 将返回错误。在这种情况下,API 不返回用户友好的错误信息。你将在以后的教程中学习使用 NestJS 进行错误处理。

定义 DELETE /articles/:id 端点

该端点是用来删除已有的文章。该端点的路由处理器名字叫 remove。如下所示:

// src/articles/articles.controller.ts

@Delete(':id')
remove(@Param('id') id: string) {
  return this.articlesService.remove(+id);
}

正如之前一样,在 ArticleService 中更新相应的方法:

// src/articles/articles.service.ts

@Injectable()
export class ArticlesService {
  constructor(private prisma: PrismaService) { }

  // ...

  remove(id: number) {
-    return `This action removes a #${id} article`;
+    return this.prisma.article.delete({ where: { id } });
  }
}

这是 articles 端点的最后一个操作。恭喜你的 API 已经完全就绪!

在 Swagger 中将端点组合在一起

ArticlesController 类中添加 @ApiTags 装饰器,在 Swagger 中将所有 articles 的端点合并成组。

// src/articles/articles.controller.ts

import { ApiTags } from '@nestjs/swagger';

@Controller('articles')
@ApiTags('articles')
export class ArticlesController {
  // ...
}

API 页面现在已经把 articles 端点组合到一起了。

articles-endpoints-grouped (1).png

更新 Swagger 响应类型

如果你查看 Swagger 中每个端点下的 Response 选项卡,你会发现 Description 还是空的。这是因为 Swagger 不知道任何端点的响应类型。你将使用一些装饰器来解决此问题。

首先,你需要定义一个实体,Swagger 可以使用该实体来识别返回的实体对象的形状。为此,请更新 article.entity.ts 文件中的 ArticleEntity 类,如下所示:

// src/articles/entities/article.entity.ts

import { Article } from '@prisma/client';
import { ApiProperty } from '@nestjs/swagger';

export class ArticleEntity implements Article {
  @ApiProperty()
  id: number;

  @ApiProperty()
  title: string;

  @ApiProperty({ required: false, nullable: true })
  description: string | null;

  @ApiProperty()
  body: string;

  @ApiProperty()
  published: boolean;

  @ApiProperty()
  createdAt: Date;

  @ApiProperty()
  updatedAt: Date;
}

这是 Prisma Client 生成的 Article 类型的实现,每个属性都添加了 @ApiProperty 装饰器。

现在,是时候用正确的响应类型注释控制器路由处理程序了。对此,NestJS 有一组专门的装饰器。

// src/articles/articles.controller.ts

+import { ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
+import { ArticleEntity } from './entities/article.entity';

@Controller('articles')
@ApiTags('articles')
export class ArticlesController {
  constructor(private readonly articlesService: ArticlesService) {}

  @Post()
+ @ApiCreatedResponse({ type: ArticleEntity })
  create(@Body() createArticleDto: CreateArticleDto) {
    return this.articlesService.create(createArticleDto);
  }

  @Get()
+ @ApiOkResponse({ type: ArticleEntity, isArray: true })
  findAll() {
    return this.articlesService.findAll();
  }

  @Get('drafts')
+ @ApiOkResponse({ type: ArticleEntity, isArray: true })
  findDrafts() {
    return this.articlesService.findDrafts();
  }

  @Get(':id')
+ @ApiOkResponse({ type: ArticleEntity })
  findOne(@Param('id') id: string) {
    return this.articlesService.findOne(+id);
  }

  @Patch(':id')
+ @ApiOkResponse({ type: ArticleEntity })
  update(@Param('id') id: string, @Body() updateArticleDto: UpdateArticleDto) {
    return this.articlesService.update(+id, updateArticleDto);
  }

  @Delete(':id')
+ @ApiOkResponse({ type: ArticleEntity })
  remove(@Param('id') id: string) {
    return this.articlesService.remove(+id);
  }
}

你已经为 GETPATCHDELETE 端点添加了 @ApiOkResponse,为 POST 端点添加了 @ApiCreateResponsetype 属性被用来指定返回类型。你可以在 NestJS 官方提供的 NestJS 文档里找到所有的响应装饰器。

现在,Swagger 已经为 API 页面上的所有端点都定义好了响应类型。

response-types (1).png

总结和最后的评论

恭喜!你已经使用 NestJS 构建了一个基本的 REST API。在本系列中,你:

  • 使用 NestJS 构建了 REST API
  • 在 NestJS 项目中完美集成了 Prisma
  • 使用 Swagger 和 Open API 文档化你的 REST API

本教程的主要内容之一是告诉你使用 NestJS 和 Prisma 构建 REST API 是多么容易。这是一个非常高效的技术栈,可用于快速构建结构良好、类型安全且可维护的后端应用程序。

你可以在 Github 上找到这个项目的源码。如果你发现问题,请随时在仓库中提出问题或提交 PR。

原文作者:Tasin Ishmam Backend web developer

原文地址:www.prisma.io/blog/nestjs…

原文发表于:2022年6月03日