
如何快速實現REST API集成以優化業務流程
getHello(): string {
return this.appService.getHello();
}
讓我們在項目文件夾中執行?npm run start:dev
?命令。這將以監視模式啟動我們的 NestJS 應用程序,并實現實時重新加載功能,即當應用程序文件發生更改時,會自動重新加載。
一旦 NestJS 運行起來,我們在 Web 瀏覽器中打開 http://localhost:3000
。這時,應該會看到一個顯示 “Hello World!” 問候語的空白頁面。
此外,我們還可以使用 API 測試工具(如 Insomnia)向 http://localhost:3000
發送 GET 請求,同樣會收到 “Hello World!” 的響應。
接下來,我們將刪除這個端點,因為它只是 Nest CLI 添加的用于演示的示例。為此,需要刪除 app.controller.ts
、app.service.ts
和 app.controller.spec.ts
文件。同時,還要在 AppController
和 app.module.ts
中刪除對這些文件的所有引用。
NestJS 的架構設計鼓勵我們按照功能來組織模塊。這種基于功能的設計將單個功能相關的代碼分組到一個文件夾中,并在一個模塊中進行注冊。這種設計方式簡化了代碼庫,使得代碼拆分變得更加容易。
在 NestJS 中,我們通過使用?@Module
?裝飾器來創建模塊。模塊用于注冊控制器、服務和任何其他需要導入的子模塊。導入的子模塊也可以注冊自己的控制器和服務。
現在,我們將使用 Nest CLI 為我們的博客文章創建一個模塊。
nest generate module posts
執行上述命令后,PostsModule
文件夾中會生成一個空的 posts.module.ts
類文件。
接下來,我們將利用 TypeScript 的 interface
來明確博客文章 JSON 對象的結構。
interface
?在 TypeScript 中被視為一種虛擬或抽象結構,它僅存在于 TypeScript 層面。interface
?主要用于 TypeScript 編譯器的類型檢查,它不會在 TypeScript 轉換為 JavaScript 的過程中生成任何 JavaScript 代碼。
現在,我們將借助 Nest CLI 來創建所需的 interface
。
cd src/posts
nest generate interface posts
這些命令會在功能相關的文件夾 /src/posts
中為我們的博客文章創建一個 posts.interface.ts
文件。
在使用 TypeScript 的 interface
關鍵字來定義接口時,請確保在接口聲明前加上 export
關鍵字,這樣就可以在整個應用程序中引用和使用該接口了。
export interface PostModel {
id?: number;
date: Date;
title: string;
body: string;
category: string;
}
在命令行提示符下,讓我們使用以下命令將當前工作目錄重置回項目的根文件夾。
cd ../..
Service 類是用于處理業務邏輯的部分。我們將要創建的?PostsService
?將專門負責處理與博客文章管理相關的業務邏輯。
接下來,我們使用 Nest CLI 來為我們的博客文章創建一個服務。
nest generate service posts
執行上述命令后,PostsService
文件夾中會生成一個空的 posts.service.ts
類文件。
@Injectable()
裝飾器的作用是將 PostsService
類標記為一個提供者(Provider),這樣我們就可以將其注冊到 PostsModule
的 providers
數組中,并隨后在控制器類中進行注入。關于這部分的詳細內容,稍后會進行詳細介紹。
Controller 是負責處理傳入的請求并向客戶端返回響應的類。一個控制器可以包含多個路由(或稱為端點),每個路由都可以實現一組特定的操作。NestJS 的路由機制會根據請求的 URL 將其路由到正確的控制器上。
現在,我們使用 Nest CLI 來為我們的博客文章創建一個控制器。
nest generate controller posts
執行上述命令后,PostsService
相關的文件夾中會生成一個空的 posts.service.ts
類文件。
接下來,我們需要將 PostsService
注入到 PostsController
類的構造函數中。
import { Controller } from '@nestjs/common';
import { PostsService } from './posts.service';
@Controller('posts')
export class PostsController {
constructor(private readonly postsService: PostsService) {}
}
NestJS 利用依賴注入機制,在控制器中自動設置對?PostsService
?的引用。這樣,我們就可以在控制器類中方便地通過?this.postsService
?來調用其提供的方法了。
我們需要確保 PostsModule
已經正確注冊了 PostsController
和 PostsService
。值得慶幸的是,之前執行的 nest generate service post
和 nest generate controller post
命令已經自動在 PostsModule
中完成了 PostsService
和 PostsController
類的注冊工作。
@Module({
controllers: [PostsController],
providers: [PostsService],
})
export class PostsModule {}
在 NestJS 中,“providers” 這一術語被用來指代 service classes(服務類)、middleware(中間件)、guards(守衛)等。
如果希望 PostsService
類能夠被我們應用程序中的其他模塊所使用,我們可以在 PostsModule
的 exports
數組中將其導出。這樣一來,任何導入了 PostsModule
的模塊都能夠訪問并使用 PostsService
。
@Module({
controllers: [PostsController],
providers: [PostsService],
exports: [PostsService],
})
export class PostsModule {}
現在,我們已經擁有了一個內聚且組織良好的模塊,它涵蓋了與博客文章相關的所有功能。但請注意,除非?AppModule
?導入了?PostsModule
,否則?PostsModule
?中的功能是無法在應用程序中使用的。
如果我們再次查看 AppModule
,會發現 PostsModule
已經通過之前執行的 nest generate module post
命令自動添加到了 imports
數組中。這意味著 AppModule
已經成功導入了 PostsModule
,從而使得 PostsModule
中的功能可以在整個應用程序中使用。
@Module({
imports: [PostsModule],
controllers: [],
providers: [],
})
export class AppModule {}
目前,我們的 PostsService
和 PostsController
都尚未實現任何功能。現在,我們將遵循 RESTful 標準來實現 CRUD(創建、讀取、更新、刪除)端點及其對應的邏輯。
NestJS 提供了與 MongoDB(通過?@nestjs/mongoose
?包)或 PostgreSQL(通過 Prisma 或 TypeORM)等數據庫集成和持久化應用數據的便捷方式。但為了演示的簡潔性,我們將在?PostsService
?中使用一個本地數組來模擬數據庫的功能。
import { Injectable } from '@nestjs/common';
import { PostModel } from './posts.interface';
@Injectable()
export class PostsService {
private posts: Array<PostModel> = [];
}
在 NestJS 中,服務(或提供者)的作用域是可以配置的。默認情況下,服務是單例的,這意味著在整個應用程序中只會存在一個服務的實例,并且該實例會被共享。服務的初始化僅在應用程序啟動時進行一次。由于服務的默認作用域是單例,因此任何注入 PostsService
的類都將訪問到內存中相同的 posts
數組數據。
現在,我們來在 PostsService
中添加一個方法,用于返回所有的博客文章。
public findAll(): Array<PostModel> {
return this.posts;
}
接下來,我們將在?PostsController
?中添加一個方法,以便將?PostsService
?的?findAll()
?方法的邏輯暴露給客戶端請求。
@Get()
public findAll(): Array<PostModel> {
return this.postsService.findAll();
}
@Get
裝飾器被用于創建一個 GET 請求的 /posts
端點。這個端點的路徑 /posts
是由定義在控制器上的 @Controller('posts')
裝飾器提供的。
讓我們為PostsService
添加一個方法,該方法能夠返回客戶端可能想要查找的特定博客文章。如果在我們維護的帖子列表中未能找到與請求中指定的帖子 ID 相匹配的條目,我們將返回一個 404 NOT FOUND HTTP 錯誤,以表明所請求的資源未找到。
public findOne(id: number): PostModel {
const post: PostModel = this.posts.find(post => post.id === id);
if (!post) {
throw new NotFoundException('Post not found.');
}
return post;
}
讓我們在PostsController
中添加一個方法,它將使服務的findAll()
方法的邏輯可用于客戶端請求。
@Get(':id')
public findOne(@Param('id', ParseIntPipe) id: number): PostModel {
return this.postsService.findOne(id);
}
在這里,@Get
裝飾器與參數裝飾器 @Param('id')
一起使用,用于創建 GET /post/:id
端點,其中 :id
是代表博客文章唯一標識的動態路由參數。
@Param
裝飾器來自 @nestjs/common
包,它能夠將路由參數作為方法參數直接提供給我們使用。需要注意的是,@Param
裝飾器獲取到的值默認是字符串類型。由于我們在 TypeScript 中將 id
定義為數字類型,因此需要進行字符串到數字的轉換。NestJS 提供了多種管道(Pipe),允許我們對請求參數進行轉換和驗證。在這里,我們可以使用 NestJS 的 ParseIntPipe
來將 id
字符串轉換為數字類型。
讓我們在PostsService
中添加一個方法,用于創建一個新的博客文章。這個方法需要為新文章分配一個順序遞增的?id
,并返回創建后的文章對象。另外,如果新文章的標題(title
)已經與現有文章重復,我們將拋出一個 422 UNPROCESSABLE ENTITY HTTP 錯誤,表示請求實體無法處理。
public create(post: PostModel): PostModel {
// if the title is already in use by another post
const titleExists: boolean = this.posts.some(
(item) => item.title === post.title,
);
if (titleExists) {
throw new UnprocessableEntityException('Post title already exists.');
}
// find the next id for a new blog post
const maxId: number = Math.max(...this.posts.map((post) => post.id), 0);
const id: number = maxId + 1;
const blogPost: PostModel = {
...post,
id,
};
this.posts.push(blogPost);
return blogPost;
}
讓我們在PostsController
中添加一個方法,它將使服務的findAll()
方法的邏輯可用于客戶端請求。
@Post()
public create(@Body() post: PostModel): PostModel {
return this.postsService.create(post);
}
@Post
裝飾器被用來創建一個 POST /post
端點。
在 NestJS 中,當我們使用?POST
、PUT
?和?PATCH
?等 HTTP 方法裝飾器時,HTTP 請求的主體(Body)通常用于向 API 傳輸數據,這些數據一般采用 JSON 格式。
為了解析 HTTP 請求的主體,我們可以使用 @Body
裝飾器。當使用這個裝飾器時,NestJS 會自動對 HTTP 請求的主體執行 JSON.parse()
操作,并將解析后的 JSON 對象作為參數傳遞給控制器的方法。在這個場景中,我們期望客戶端發送的數據符合 Post
類型的結構,因此在 @Body
裝飾器中,我們將參數類型聲明為 Post
。
讓我們在PostsService
中添加一個方法,該方法使用 JavaScript 的?splice()
?方法從內存中的帖子數組中移除指定的博客帖子。如果在我們維護的帖子列表中找不到與請求中指定的帖子 ID 相匹配的條目,我們將返回一個 404 NOT FOUND HTTP 錯誤,表明所請求的資源未找到。
public delete(id: number): void {
const index: number = this.posts.findIndex(post => post.id === id);
// -1 is returned when no findIndex() match is found
if (index === -1) {
throw new NotFoundException('Post not found.');
}
this.posts.splice(index, 1);
}
讓我們在PostsController
中添加一個方法,它將使服務的findAll()
方法的邏輯可用于客戶端請求。
@Delete(':id')
public delete(@Param('id', ParseIntPipe) id: number): void {
this.postsService.delete(id);
}
讓我們為PostsService
添加一個方法,用于查找具有指定?id
?的博客文章,并使用新提交的數據對其進行更新。更新完成后,該方法將返回更新后的文章對象。如果在我們的帖子列表中未找到與請求中指定的?id
?相匹配的條目,我們將返回一個 404 NOT FOUND HTTP 錯誤,表明所請求的資源未找到。另外,如果新提交的標題(title
)已經被其他博客文章使用,我們將拋出一個 422 UNPROCESSABLE ENTITY HTTP 錯誤,表示請求實體無法處理。
public update(id: number, post: PostModel): PostModel {
this.logger.log(Updating post with id: ${id}
);
const index: number = this.posts.findIndex((post) => post.id === id);
// -1 is returned when no findIndex() match is found
if (index === -1) {
throw new NotFoundException('Post not found.');
}
// if the title is already in use by another post
const titleExists: boolean = this.posts.some(
(item) => item.title === post.title && item.id !== id,
);
if (titleExists) {
throw new UnprocessableEntityException('Post title already exists.');
}
const blogPost: PostModel = {
...post,
id,
};
this.posts[index] = blogPost;
return blogPost;
}
讓我們在PostsController
中添加一個方法,它將使服務的findAll()
方法的邏輯可用于客戶端請求。
@Put(':id')
public update(@Param('id', ParseIntPipe) id: number, @Body() post: PostModel): PostModel {
return this.postsService.update(id, post);
}
我們使用 @Put
裝飾器來處理 HTTP PUT 請求方法。PUT 方法既可以用于創建新資源,也可以用于更新服務器上已有資源的狀態。當服務器上的資源已存在且我們知道其位置時,PUT 請求將替換該資源的當前狀態。
首先,使用 npm run start:dev
命令啟動我們的開發服務器。然后,打開 Incubator 應用程序,以便測試我們為 PostsModule
創建的 API 端點。
向?http://localhost:3000/posts
?發送 GET 請求。預期結果是一個表示空數組的響應,并附帶 200 OK 成功狀態碼。
接下來,向 http://localhost:3000/posts
發送 POST 請求,并在請求體中包含以下 JSON 數據:
{
"date": "2021-08-16",
"title": "Intro to NestJS",
"body": "This blog post is about NestJS",
"category": "NestJS"
}
我們應該會收到一個 201 Created 響應代碼,表示帖子已成功創建。同時,響應體中還會包含一個 JSON 對象,該對象表示已創建的帖子,并包括一個自動生成的?id
?字段。
接下來,向? http://localhost:3000/posts/1
?發送 GET 請求。預期結果是一個 200 OK 響應代碼,以及包含帖子數據的響應體,其中?id
?字段的值為 1。
讓我們使用下面的 JSON 數據體向 http://localhost:3000/posts
發送一個 POST 請求。
{
"date": "2021-08-16",
"title": "Intro to TypeScript",
"body": "An intro to TypeScript",
"category": "TypeScript"
}
結果應該是一個 200 OK 響應代碼,同時響應體中會包含一個 JSON 對象,該對象表示已更新后的帖子。
接下來,向 http://localhost:3000/posts/1
發送 DELETE 請求。預期結果是一個 200 OK 響應代碼,并且響應體中不包含任何 JSON 對象。
NestJS 使得在應用程序中添加日志記錄變得非常簡單。我們應該使用 NestJS 提供的日志記錄功能,而不是直接使用 console.log()
語句或原生的 Logger
類。NestJS 的日志記錄功能會在終端中為我們提供格式良好、易于閱讀的日志消息。
要在我們的 API 中添加日志記錄,第一步是在服務類中定義一個 logger 實例。
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { PostModel } from './posts.interface';
@Injectable()
export class PostsService {
private posts: Array<PostModel> = [];
private readonly logger = new Logger(PostsService.name);
// ...
}
現在我們已經定義了日志記錄器,接下來可以在服務類中添加日志語句。以下是一個日志語句的示例,我們可以將它作為 findAll()
方法的第一行代碼添加到 PostsService
類中。
this.logger.log('Returning all posts');
在客戶端向我們的 API 發起請求時,每次調用服務方法,這類日志語句都會在終端上輸出相應的日志消息,為我們提供便利。
當發送GET /posts
請求時,我們應該在終端中看到以下消息。
[PostsService] Returning all posts.
NestJS 使得利用 NestJS Swagger 包將 OpenAPI 規范集成到我們的 API 中變得輕而易舉。OpenAPI 規范是一種用于描述 RESTful API 的標準,它主要用于文檔化和提供參考信息。
讓我們為 NestJS 安裝 Swagger。
npm install --save @nestjs/swagger swagger-ui-express
讓我們通過添加Swagger配置來更新引導NestJS應用程序的main.ts
文件。
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('Blog API')
.setDescription('Blog API')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
await app.listen(3000);
}
bootstrap();
當 NestJS 應用程序正在運行時,我們現在可以訪問 http://localhost:3000/api
來查看 API 的 Swagger 文檔。請注意,默認情況下,“default”會顯示在我們帖子相關路由的上方作為標簽。
為了改變這一點,我們可以在 PostsController
類中的 @Controller('posts')
裝飾器下方添加 @ApiTags('posts')
裝飾器。這將用 “posts” 替換 “default”,以清晰地表明這組端點屬于 “posts” 功能或特性集。
@Controller('posts')
@ApiTags('posts')
為了使 PostModel
接口中的屬性對 Swagger 可見,我們需要使用 @ApiProperty()
裝飾器(對于必填字段)或 @ApiPropertyOptional()
裝飾器(對于可選字段)來注釋這些字段。但請注意,由于裝飾器通常用于類屬性,我們需要將接口更改為類,以便能夠應用這些裝飾器。
因此,我們的下一步是將 posts.interface.ts
文件中的 PostModel
接口轉換為類,并在相應的屬性上使用 @ApiProperty()
或 @ApiPropertyOptional()
裝飾器。
export class PostModel {
@ApiPropertyOptional({ type: Number })
id?: number;
@ApiProperty({ type: String, format: 'date-time' })
date: Date;
@ApiProperty({ type: String })
title: string;
@ApiProperty({ type: String })
body: string;
@ApiProperty({ type: String })
category: string;
}
在?@ApiProperty()
?裝飾器中,我們為每個字段指定了類型。值得注意的是,id
?字段被標記為可選,因為在創建新的博客文章時,我們通常不知道它的?id
?會是什么(通常由數據庫自動生成)。同時,date
?字段被指定為使用?date-time
?字符串格式。
這些更改使得 PostModel
的結構能夠在 Swagger 中得到正確的記錄。當 NestJS 應用程序運行時,我們可以訪問 http://localhost:3000/api
來查看 PostModel
的詳細文檔。
接下來,我們將利用 Swagger 的?@ApiResponse()
?裝飾器來全面記錄 API 端點可能返回的所有響應類型。這樣做有助于我們的 API 用戶清晰地了解,通過調用特定的端點,他們可以獲得哪些類型的響應。我們將在?PostsController
?類中實施這些更改。
對于 findAll
方法,我們將使用 @ApiOkResponse()
裝飾器來明確記錄 200 OK 成功響應的詳細信息。
@Get()
@ApiOkResponse({ description: 'Posts retrieved successfully.'})
public findAll(): Array<PostModel> {
return this.postsService.findAll();
}
對于 findOne
方法,當成功找到對應的帖子時,我們使用 @ApiOkResponse()
裝飾器來記錄 200 OK 響應。而當未找到帖子時,我們則使用 @ApiNotFoundResponse()
裝飾器來記錄 404 NOT FOUND HTTP 錯誤。
@Get(':id')
@ApiOkResponse({ description: 'Post retrieved successfully.'})
@ApiNotFoundResponse({ description: 'Post not found.' })
public findOne(@Param('id', ParseIntPipe) id: number): PostModel {
return this.postsService.findOne(id);
}
對于?create
?方法,當成功創建一個新的帖子時,我們將使用?@ApiCreatedResponse()
?裝飾器來記錄 201 CREATED 響應。而當檢測到重復的文章標題時,我們會使用?@ApiUnprocessableEntityResponse()
?裝飾器來記錄一個 422 UNPROCESSABLE ENTITY HTTP 錯誤。
@Post()
@ApiCreatedResponse({ description: 'Post created successfully.' })
@ApiUnprocessableEntityResponse({ description: 'Post title already exists.' })
public create(@Body() post: PostModel): void {
return this.postsService.create(post);
}
對于?delete
?方法,如果帖子被成功刪除,我們將使用?@ApiOkResponse()
?裝飾器來記錄 200 OK 響應。而當嘗試刪除一個不存在的帖子時,我們會使用?@ApiNotFoundResponse()
?裝飾器來記錄一個 404 NOT FOUND HTTP 錯誤。
@Delete(':id')
@ApiOkResponse({ description: 'Post deleted successfully.'})
@ApiNotFoundResponse({ description: 'Post not found.' })
public delete(@Param('id', ParseIntPipe) id: number): void {
return this.postsService.delete(id);
}
對于 update
方法,當帖子被成功更新時,我們將使用 @ApiOkResponse()
裝飾器來記錄 200 OK 響應。如果嘗試更新一個不存在的帖子,我們會使用 @ApiNotFoundResponse()
裝飾器來記錄一個 404 NOT FOUND HTTP 錯誤。另外,當發現存在重復的帖子標題時,我們會使用 @ApiUnprocessableEntityResponse()
裝飾器來記錄一個 422 UNPROCESSABLE ENTITY HTTP 錯誤。
@Put(':id')
@ApiOkResponse({ description: 'Post updated successfully.'})
@ApiNotFoundResponse({ description: 'Post not found.' })
@ApiUnprocessableEntityResponse({ description: 'Post title already exists.' })
public update(@Param('id', ParseIntPipe) id: number, @Body() post: PostModel): void {
return this.postsService.update(id, post);
}
保存上述更改后,現在您應該能夠在 Swagger 網頁的 http://localhost:3000/api
地址上查看到每個端點的所有響應代碼及其相應的描述信息。
此外,我們可以利用 Swagger 來測試我們的 API,而不僅僅是依賴 Inclusive。只需在 Swagger 網頁上點擊每個端點下方的“Try it out”按鈕,即可輕松驗證這些端點是否按預期正常工作。
異常過濾器為我們提供了對 NestJS 異常處理層的全面掌控。通過它,我們可以為 HTTP 異常響應主體添加自定義字段,或者記錄終端上發生的每個 HTTP 異常的日志信息。
接下來,我們需要在 /src/filters
文件夾中創建一個新的文件,命名為 http-exception.filter.ts
。然后,在這個文件中,我們將定義一個異常過濾器類。
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, Logger } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(HttpExceptionFilter.name);
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const statusCode = exception.getStatus();
const message = exception.message || null;
const body = {
statusCode,
message,
timestamp: new Date().toISOString(),
endpoint: request.url,
};
this.logger.warn(${statusCode} ${message}
);
response
.status(statusCode)
.json(body);
}
}
這個類會利用 NestJS 的記錄器功能,在 HTTP 異常產生時向終端輸出警告信息。同時,當 HTTP 異常發生時,它還會在響應體中包含兩個自定義字段:timestamp
?字段用于記錄異常發生的時間點,而?endpoint
?字段則用于指明是哪個路由觸發了該異常。
為了將這個過濾器應用到 PostsController
上,我們需要使用 @UseFilters(HttpExceptionFilter)
裝飾器,并傳入 HttpExceptionFilter
類的一個新實例。
@Controller('posts')
@UseFilters(new HttpExceptionFilter())
export class PostsController {
constructor(private readonly postsService: PostsService) {}
}
保存這些更改后,NestJS將重新加載我們的應用程序。如果我們使用Inclusion向我們的API發送一個PUT /posts/1
請求,它應該會觸發一個404 NOT FOUND
HTTP錯誤,因為當它啟動時,我們的應用程序中不存在可供更新的博客文章。返回到Incubator的HTTP異常響應主體現在應該包含timestamp
和endpoint
字段。
{
"statusCode": 404,
"message": "Post not found.",
"timestamp": "2021-08-23T21:05:29.497Z",
"endpoint": "/posts/1"
}
我們還應該看到下面這行打印到終端。
WARN [HttpExceptionFilter] 404 Post not found.
在本文中,我們深入了解了 NestJS 如何讓后端 API 開發變得迅速、簡潔且高效。NestJS 提供的應用程序結構助力我們構建出結構清晰、組織有序的項目。
我們涵蓋了很多內容,所以讓我們回顧一下我們學到的內容:
希望你在使用 NestJS 進行開發時能夠感受到它的強大與便捷,享受開發的樂趣!
原文鏈接:https://www.thisdot.co/blog/introduction-to-restful-apis-with-nestjs