構建 Node.js/TypeScript REST API,第 2 部分:模型、中間件和服務

已發表: 2022-03-11

在我們 REST API 系列的第一篇文章中,我們介紹瞭如何使用 npm 從頭開始創建後端、添加 TypeScript 等依賴項、使用 Node.js 中內置的debug模塊、構建 Express.js 項目結構以及記錄運行時與溫斯頓一起靈活地舉辦活動。 如果您已經對這些概念感到滿意,只需克隆它,使用git checkout切換到toptal-article-01分支,然後繼續閱讀。

REST API 服務、中間件、控制器和模型

正如所承諾的,我們現在將詳細了解這些模塊:

  • 通過將業務邏輯操作封裝到中間件和控制器可以調用的函數中,使我們的代碼更簡潔的服務。
  • 在 Express.js 調用適當的控制器函數之前驗證先決條件的中間件
  • 在最終向請求者發送響應之前使用服務處理請求的控制器
  • 描述我們的數據並幫助編譯時檢查的模型

我們還將添加一個非常不適合生產的非常基本的數據庫。 (它的唯一目的是使本教程更易於理解,為我們的下一篇文章深入研究數據庫連接以及與 MongoDB 和 Mongoose 的集成鋪平道路。)

動手實踐:DAO、DTO 和我們的臨時數據庫的第一步

對於我們教程的這一部分,我們的數據庫甚至不會使用文件。 它只會將用戶數據保存在一個數組中,這意味著只要我們退出 Node.js,數據就會消失。 它將僅支持最基本的創建、讀取、更新和刪除(CRUD) 操作。

我們將在這裡使用兩個概念:

  • 數據訪問對象 (DAO)
  • 數據傳輸對象 (DTO)

首字母縮寫詞之間的一個字母差異是必不可少的:DAO 負責連接到定義的數據庫並執行 CRUD 操作; DTO 是一個對象,它保存 DAO 將發送到數據庫以及從數據庫接收的原始數據。

換句話說,DTO 是符合數據模型類型的對象,而 DAO 是使用它們的服務。

雖然 DTO 可能會變得更複雜(例如,表示嵌套的數據庫實體),但在本文中,單個 DTO 實例將對應於單個數據庫行上的特定操作。

為什麼是 DTO?

使用 DTO 使我們的 TypeScript 對象符合我們的數據模型有助於保持架構的一致性,正如我們將在下面的服務部分中看到的那樣。 但是有一個重要的警告:DTO 和 TypeScript 本身都不承諾任何類型的自動用戶輸入驗證,因為這必須在運行時發生。 當我們的代碼在 API 的端點接收用戶輸入時,該輸入可能:

  • 有額外的字段
  • 缺少必填字段(即那些不帶?後綴的字段)
  • 具有數據不是我們使用 TypeScript 在模型中指定的類型的字段

TypeScript(以及它轉譯成的 JavaScript)不會為我們神奇地檢查這一點,因此不要忘記這些驗證非常重要,尤其是在向公眾開放您的 API 時。 像 ajv 這樣的包可以幫助解決這個問題,但通常通過在特定於庫的模式對象而不是本機 TypeScript 中定義模型來工作。 (在下一篇文章中討論的 Mongoose 將在這個項目中扮演類似的角色。)

你可能會想,“真的最好同時使用 DAO 和 DTO,而不是更簡單的東西嗎?” 企業開發人員 Gunther Popp 給出了答案; 您將希望在大多數較小的現實世界 Express.js/TypeScript 項目中避免 DTO,除非您可以合理地期望在中期進行擴展。

但是,即使您不打算在生產中使用它們,這個示例項目也是掌握 TypeScript API 架構的一個有價值的機會。 這是練習以其他方式利用 TypeScript 類型並使用 DTO 來查看它們在添加組件和模型時與更基本的方法相比如何的好方法。

我們在 TypeScript 級別的用戶 REST API 模型

首先,我們將為我們的用戶定義三個 DTO。 讓我們在users文件夾中創建一個名為dto的文件夾,並在那裡創建一個名為create.user.dto.ts的文件,其中包含以下內容:

 export interface CreateUserDto { id: string; email: string; password: string; firstName?: string; lastName?: string; permissionLevel?: number; }

我們是說每次創建用戶時,無論數據庫如何,它都應該有一個 ID、密碼和電子郵件,以及可選的名字和姓氏。 這些需求可以根據給定項目的業務需求而改變。

對於PUT請求,我們想要更新整個對象,因此現在需要我們的可選字段。 在同一文件夾中,使用以下代碼創建一個名為put.user.dto.ts的文件:

 export interface PutUserDto { id: string; email: string; password: string; firstName: string; lastName: string; permissionLevel: number; }

對於PATCH請求,我們可以使用 TypeScript 的Partial功能,它通過複製另一個類型並使其所有字段可選來創建新類型。 這樣,文件patch.user.dto.ts只需要包含以下代碼:

 import { PutUserDto } from './put.user.dto'; export interface PatchUserDto extends Partial<PutUserDto> {}

現在,讓我們創建內存中的臨時數據庫。 讓我們在users文件夾中創建一個名為daos的文件夾,並添加一個名為users.dao.ts的文件。

首先,我們要導入我們創建的 DTO:

 import { CreateUserDto } from '../dto/create.user.dto'; import { PatchUserDto } from '../dto/patch.user.dto'; import { PutUserDto } from '../dto/put.user.dto';

現在,為了處理我們的用戶 ID,讓我們添加 shortid 庫(使用終端):

 npm i shortid npm i --save-dev @types/shortid

回到users.dao.ts ,我們將導入 shortid:

 import shortid from 'shortid'; import debug from 'debug'; const log: debug.IDebugger = debug('app:in-memory-dao');

我們現在可以創建一個名為UsersDao的類,如下所示:

 class UsersDao { users: Array<CreateUserDto> = []; constructor() { log('Created new instance of UsersDao'); } } export default new UsersDao();

使用單例模式,當我們將它導入其他文件時,這個類將始終提供相同的實例——而且至關重要的是,相同的users數組。 這是因為 Node.js 會在任何導入的位置緩存此文件,並且所有導入都發生在啟動時。 也就是說,任何引用users.dao.ts的文件都將被傳遞給 Node.js 第一次處理此文件時導出的同一個new UsersDao()的引用。

當我們在本文中進一步使用這個類時,我們將看到它的工作原理,並將這種常見的 TypeScript/Express.js 模式用於整個項目中的大多數類。

注意:單例的一個經常被引用的缺點是它們很難編寫單元測試。 在我們的許多類的情況下,這個缺點將不適用,因為沒有任何需要重置的類成員變量。 但是對於那些需要的人,我們將其作為練習留給讀者考慮使用依賴注入來解決這個問題。

現在我們要將基本的 CRUD 操作作為函數添加到類中。 創建函數將如下所示:

 async addUser(user: CreateUserDto) { user.id = shortid.generate(); this.users.push(user); return user.id; }

讀取將有兩種形式,“讀取所有資源”和“按 ID 讀取一個”。 它們的編碼如下:

 async getUsers() { return this.users; } async getUserById(userId: string) { return this.users.find((user: { id: string }) => user.id === userId); }

同樣,更新將意味著覆蓋整個對象(作為PUT )或僅覆蓋對象的一部分(作為PATCH ):

 async putUserById(userId: string, user: PutUserDto) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); this.users.splice(objIndex, 1, user); return `${user.id} updated via put`; } async patchUserById(userId: string, user: PatchUserDto) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); let currentUser = this.users[objIndex]; const allowedPatchFields = [ 'password', 'firstName', 'lastName', 'permissionLevel', ]; for (let field of allowedPatchFields) { if (field in user) { // @ts-ignore currentUser[field] = user[field]; } } this.users.splice(objIndex, 1, currentUser); return `${user.id} patched`; }

如前所述,儘管我們在這些函數簽名中聲明了UserDto ,但 TypeScript 不提供運行時類型檢查。 這意味著:

  • putUserById()有一個錯誤。 它將允許 API 使用者存儲不屬於我們的 DTO 定義的模型的字段的值。
  • patchUserById()依賴於必須與模型保持同步的字段名稱的重複列表。 如果沒有這個,它將不得不使用正在為這個列表更新的對象。 這意味著它將默默地忽略作為 DTO 定義模型的一部分但之前沒有為該特定對象實例保存的字段的值。

但是這兩種情況都將在下一篇文章中在數據庫級別正確處理。

最後一個操作是刪除資源,如下所示:

 async removeUserById(userId: string) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); this.users.splice(objIndex, 1); return `${userId} removed`; }

作為獎勵,知道創建用戶的先決條件是驗證用戶電子郵件是否不重複,現在讓我們添加一個“通過電子郵件獲取用戶”功能:

 async getUserByEmail(email: string) { const objIndex = this.users.findIndex( (obj: { email: string }) => obj.email === email ); let currentUser = this.users[objIndex]; if (currentUser) { return currentUser; } else { return null; } }

注意:在實際場景中,您可能會使用預先存在的庫(例如 Mongoose 或 Sequelize)連接到數據庫,這將抽象您可能需要的所有基本操作。 因此,我們不會詳細介紹上面實現的功能。

我們的 REST API 服務層

現在我們有了一個基本的內存 DAO,我們可以創建一個調用 CRUD 函數的服務。 由於 CRUD 函數是每個連接到數據庫的服務都需要擁有的東西,因此我們將創建一個CRUD接口,其中包含每次我們想要實現新服務時想要實現的方法。

如今,我們使用的 IDE 具有代碼生成功能,可以添加我們正​​在實現的功能,從而減少我們需要編寫的重複代碼量。

使用 WebStorm IDE 的快速示例:

WebStorm 的屏幕截圖顯示了一個名為 MyService 的類的空定義,該類實現了一個名為 CRUD 的接口。 IDE 用紅色下劃線為 MyService 名稱添加了下劃線。

IDE 突出顯示 MyService 類名稱並建議以下選項:

與上一個類似的屏幕截圖,但上下文菜單列出了幾個選項,其中第一個是“實施所有成員”。

“實現所有成員”選項立即搭建了符合CRUD接口所需的功能:

WebStorm 中 MyService 類的屏幕截圖。 MyService 不再用紅色下劃線,並且類定義現在包含在 CRUD 接口中指定的所有 TypeScript 類型的函數簽名(以及函數體,無論是空的還是包含返回語句)。

說了這麼多,讓我們首先創建我們的 TypeScript 接口,稱為CRUD 。 在我們的common文件夾中,讓我們創建一個名為interfaces的文件夾並添加crud.interface.ts並添加以下內容:

 export interface CRUD { list: (limit: number, page: number) => Promise<any>; create: (resource: any) => Promise<any>; putById: (id: string, resource: any) => Promise<string>; readById: (id: string) => Promise<any>; deleteById: (id: string) => Promise<string>; patchById: (id: string, resource: any) => Promise<string>; }

完成後,讓我們在users文件夾中創建一個services文件夾,並在其中添加users.service.ts文件,其中包含:

 import UsersDao from '../daos/users.dao'; import { CRUD } from '../../common/interfaces/crud.interface'; import { CreateUserDto } from '../dto/create.user.dto'; import { PutUserDto } from '../dto/put.user.dto'; import { PatchUserDto } from '../dto/patch.user.dto'; class UsersService implements CRUD { async create(resource: CreateUserDto) { return UsersDao.addUser(resource); } async deleteById(id: string) { return UsersDao.removeUserById(id); } async list(limit: number, page: number) { return UsersDao.getUsers(); } async patchById(id: string, resource: PatchUserDto) { return UsersDao.patchUserById(id, resource); } async readById(id: string) { return UsersDao.getUserById(id); } async putById(id: string, resource: PutUserDto) { return UsersDao.putUserById(id, resource); } async getUserByEmail(email: string) { return UsersDao.getUserByEmail(email); } } export default new UsersService();

我們的第一步是導入內存中的 DAO、接口依賴項和每個 DTO 的 TypeScript 類型,是時候將UsersService實現為服務單例了,這與我們在 DAO 中使用的模式相同。

現在所有的CRUD函數都只是調用UsersDao的相應函數。 當需要替換 DAO 時,我們不必在項目中的其他任何地方進行更改,除了對調用 DAO 函數的這個文件進行一些調整,正如我們將在第 3 部分中看到的那樣。

例如,我們不必跟踪對list()的每個調用並在替換它之前檢查它的上下文。 這就是擁有這一層分離的優勢,代價是您在上面看到的少量初始樣板。

異步/等待和 Node.js

我們對服務功能使用async似乎毫無意義。 現在,它是:所有這些函數只是立即返回它們的值,沒有任何內部使用Promiseawait 。 這僅是為將使用async的服務準備我們的代碼庫。 同樣,在下面,您將看到對這些函數的所有調用都使用await

在本文結束時,您將再次擁有一個可運行的項目來進行試驗。 那將是嘗試在代碼庫的不同位置添加各種類型的錯誤並查看編譯和測試期間發生的情況的絕佳時機。 特別是async上下文中的錯誤可能與您預期的不太一樣。 值得挖掘和探索各種解決方案,這超出了本文的範圍。


現在,準備好 DAO 和服務,讓我們回到用戶控制器。

構建我們的 REST API 控制器

正如我們上面所說,控制器背後的想法是將路由配置與最終處理路由請求的代碼分開。 這意味著所有的驗證都應該在我們的請求到達控制器之前完成。 控制器只需要知道如何處理實際請求,因為如果請求達到了那麼遠,那麼我們就知道它是有效的。 然後控制器將調用它將處理的每個請求的相應服務。

在開始之前,我們需要安裝一個庫來安全地散列用戶密碼:

 npm i argon2

讓我們首先在users控制器文件夾中創建一個名為controllers的文件夾,並在其中創建一個名為users.controller.ts的文件:

 // we import express to add types to the request/response objects from our controller functions import express from 'express'; // we import our newly created user services import usersService from '../services/users.service'; // we import the argon2 library for password hashing import argon2 from 'argon2'; // we use debug with a custom context as described in Part 1 import debug from 'debug'; const log: debug.IDebugger = debug('app:users-controller'); class UsersController { async listUsers(req: express.Request, res: express.Response) { const users = await usersService.list(100, 0); res.status(200).send(users); } async getUserById(req: express.Request, res: express.Response) { const user = await usersService.readById(req.body.id); res.status(200).send(user); } async createUser(req: express.Request, res: express.Response) { req.body.password = await argon2.hash(req.body.password); const userId = await usersService.create(req.body); res.status(201).send({ id: userId }); } async patch(req: express.Request, res: express.Response) { if (req.body.password) { req.body.password = await argon2.hash(req.body.password); } log(await usersService.patchById(req.body.id, req.body)); res.status(204).send(); } async put(req: express.Request, res: express.Response) { req.body.password = await argon2.hash(req.body.password); log(await usersService.putById(req.body.id, req.body)); res.status(204).send(); } async removeUser(req: express.Request, res: express.Response) { log(await usersService.deleteById(req.body.id)); res.status(204).send(); } } export default new UsersController();

注意:上面的行不發送任何內容並返回HTTP 204 No Content響應與該主題的 RFC 7231 一致。

完成我們的用戶控制器單例後,我們準備編寫依賴於示例 REST API 對像模型和服務的另一個模塊:我們的用戶中間件。

帶有 Express.js 的 Node.js REST 中間件

我們可以用 Express.js 中間件做什麼? 對於一個人來說,驗證非常適合。 讓我們添加一些基本的驗證來充當請求的看門人,然後再將請求發送到我們的用戶控制器:

  • 確保存在創建或更新用戶所需的用戶字段,例如emailpassword
  • 確保給定的電子郵件尚未使用
  • 檢查我們在創建後沒有更改email字段(因為為簡單起見,我們將其用作面向用戶的主要 ID)
  • 驗證給定用戶是否存在

為了使這些驗證與 Express.js 一起使用,我們需要將它們轉換為遵循 Express.js 使用next()的流控制模式的函數,如上一篇文章中所述。 我們需要一個新文件users/middleware/users.middleware.ts

 import express from 'express'; import userService from '../services/users.service'; import debug from 'debug'; const log: debug.IDebugger = debug('app:users-controller'); class UsersMiddleware { } export default new UsersMiddleware();

使用熟悉的單例樣板,讓我們將一些中間件函數添加到類主體中:

 async validateRequiredUserBodyFields( req: express.Request, res: express.Response, next: express.NextFunction ) { if (req.body && req.body.email && req.body.password) { next(); } else { res.status(400).send({ error: `Missing required fields email and password`, }); } } async validateSameEmailDoesntExist( req: express.Request, res: express.Response, next: express.NextFunction ) { const user = await userService.getUserByEmail(req.body.email); if (user) { res.status(400).send({ error: `User email already exists` }); } else { next(); } } async validateSameEmailBelongToSameUser( req: express.Request, res: express.Response, next: express.NextFunction ) { const user = await userService.getUserByEmail(req.body.email); if (user && user.id === req.params.userId) { next(); } else { res.status(400).send({ error: `Invalid email` }); } } // Here we need to use an arrow function to bind `this` correctly validatePatchEmail = async ( req: express.Request, res: express.Response, next: express.NextFunction ) => { if (req.body.email) { log('Validating email', req.body.email); this.validateSameEmailBelongToSameUser(req, res, next); } else { next(); } }; async validateUserExists( req: express.Request, res: express.Response, next: express.NextFunction ) { const user = await userService.readById(req.params.userId); if (user) { next(); } else { res.status(404).send({ error: `User ${req.params.userId} not found`, }); } }

為了讓我們的 API 使用者更容易地對新添加的用戶發出進一步的請求,我們將添加一個輔助函數,該函數將從請求參數中提取userId (來自請求 URL 本身)並將其添加到請求正文,其餘用戶數據所在的位置。

這裡的想法是,當我們想更新用戶信息時,能夠簡單地使用完整的請求,而不必擔心每次都從參數中獲取 ID。 相反,它只在一個地方處理,即中間件。 該函數將如下所示:

 async extractUserId( req: express.Request, res: express.Response, next: express.NextFunction ) { req.body.id = req.params.userId; next(); }

除了邏輯之外,中間件和控制器之間的主要區別在於,現在我們使用next()函數沿著一系列配置的函數傳遞控制權,直到它到達最終目的地,在我們的例子中是控制器。

放在一起:重構我們的路線

現在我們已經實現了項目架構的所有新方面,讓我們回到我們在上一篇文章中定義的users.routes.config.ts文件。 它將調用我們的中間件和控制器,它們都依賴於我們的用戶服務,而這又需要我們的用戶模型。

最終文件將像這樣簡單:

 import { CommonRoutesConfig } from '../common/common.routes.config'; import UsersController from './controllers/users.controller'; import UsersMiddleware from './middleware/users.middleware'; import express from 'express'; export class UsersRoutes extends CommonRoutesConfig { constructor(app: express.Application) { super(app, 'UsersRoutes'); } configureRoutes(): express.Application { this.app .route(`/users`) .get(UsersController.listUsers) .post( UsersMiddleware.validateRequiredUserBodyFields, UsersMiddleware.validateSameEmailDoesntExist, UsersController.createUser ); this.app.param(`userId`, UsersMiddleware.extractUserId); this.app .route(`/users/:userId`) .all(UsersMiddleware.validateUserExists) .get(UsersController.getUserById) .delete(UsersController.removeUser); this.app.put(`/users/:userId`, [ UsersMiddleware.validateRequiredUserBodyFields, UsersMiddleware.validateSameEmailBelongToSameUser, UsersController.put, ]); this.app.patch(`/users/:userId`, [ UsersMiddleware.validatePatchEmail, UsersController.patch, ]); return this.app; } }

在這裡,我們通過添加中間件來驗證我們的業務邏輯和適當的控制器函數來重新定義我們的路由,以在一切都有效的情況下處理請求。 我們還使用了 Express.js 中的.param()函數來提取userId

.all()函數中,我們從UsersMiddleware傳遞我們的validateUserExists函數,以便在任何GETPUTPATCHDELETE可以通過端點/users/:userId之前調用。 這意味著validateUserExists不需要在我們傳遞給.put().patch() () 的附加函數數組中——它將在此處指定的函數之前被調用。

我們也以另一種方式利用了中間件固有的可重用性。 通過傳遞將在POSTPUT上下文中使用的UsersMiddleware.validateRequiredUserBodyFields ,我們可以優雅地將其與其他中間件函數重新組合。

免責聲明:我們僅在本文中介紹基本驗證。 在現實世界的項目中,您需要考慮並找到編碼所需的所有限制。 為了簡單起見,我們還假設用戶不能更改他們的電子郵件。

測試我們的 Express/TypeScript REST API

我們現在可以編譯並運行我們的 Node.js 應用程序。 一旦它運行起來,我們就可以使用 REST 客戶端(例如 Postman 或 cURL)來測試我們的 API 路由。

讓我們首先嘗試獲取我們的用戶:

 curl --request GET 'localhost:3000/users' \ --header 'Content-Type: application/json'

此時,我們將有一個空數組作為響應,這是準確的。 現在我們可以嘗試用這個創建第一個用戶資源:

 curl --request POST 'localhost:3000/users' \ --header 'Content-Type: application/json'

請注意,現在我們的 Node.js 應用程序將從我們的中間件發回一個錯誤:

 { "error": "Missing required fields email and password" }

為了解決這個問題,讓我們發送一個有效的請求來發佈到/users資源:

 curl --request POST 'localhost:3000/users' \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "[email protected]", "password": "sup3rS3cr3tPassw0rd!23" }'

這一次,我們應該看到如下內容:

 { "id": "ksVnfnPVW" }

id是新創建用戶的標識符,在您的機器上會有所不同。 為了使剩餘的測試語句更容易,您可以使用您獲得的命令運行此命令(假設您使用的是類似 Linux 的環境):

 REST_API_EXAMPLE_

我們現在可以看到使用上述變量發出GET請求得到的響應:

 curl --request GET "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json'

我們現在還可以使用以下PUT請求更新整個資源:

 curl --request PUT "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "[email protected]", "password": "sup3rS3cr3tPassw0rd!23", "firstName": "Marcos", "lastName": "Silva", "permissionLevel": 8 }'

我們還可以通過更改電子郵件地址來測試我們的驗證是否有效,這應該會導致錯誤。

請注意,當使用PUT到資源 ID 時,作為 API 使用者,如果我們想要符合標準 REST 模式,則需要發送整個對象。 這意味著如果我們只想更新lastName字段,但使用我們的PUT端點,我們將被迫發送要更新的整個對象。 使用PATCH請求會更容易,因為它仍然在標準 REST 約束範圍內,只發送lastName字段:

 curl --request PATCH "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' \ --data-raw '{ "lastName": "Faraco" }'

回想一下,在我們自己的代碼庫中,使用我們在本文中添加的中間件函數強制區分PUTPATCH的是我們的路由配置。

PUTPATCH或兩者兼而有之?

鑑於PATCH的靈活性,聽起來似乎沒有太多理由支持PUT ,並且一些 API 將採用這種方法。 其他人可能堅持支持PUT以使 API “完全符合 REST”,在這種情況下,創建每個字段的PUT路由可能是常見用例的適當策略。

實際上,這些觀點是更大討論的一部分,範圍從兩者之間的現實差異到更靈活的PATCH語義。 為了簡單起見,我們在此處提供PUT支持和廣泛使用的PATCH語義,但鼓勵讀者在準備好時深入研究。

像上面一樣再次獲取用戶列表,我們應該會看到我們創建的用戶的字段已更新:

 [ { "id": "ksVnfnPVW", "email": "[email protected]", "password": "$argon2i$v=19$m=4096,t=3,p=1$ZWXdiTgb922OvkNAdh9acA$XUXsOHaRN4uVg5ltIwwO+SPLxvb9uhOKcxoLER1e/mM", "firstName": "Marcos", "lastName": "Faraco", "permissionLevel": 8 } ]

最後,我們可以用這個來測試刪除用戶:

 curl --request DELETE "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json'

再次獲取用戶列表,我們應該看到已刪除的用戶不再存在。

這樣,我們就可以為users資源工作的所有 CRUD 操作。

Node.js/TypeScript REST API

在本系列的這一部分中,我們進一步探索了使用 Express.js 構建 REST API 的關鍵步驟。 我們拆分代碼以支持服務、中間件、控制器和模型。 它們的每個功能都有特定的作用,無論是驗證、邏輯操作,還是處理有效請求並響應它們。

我們還創建了一種非常簡單的數據存儲方法,其目的是(請原諒雙關語)在這一點上允許進行一些測試,然後在我們系列的下一部分中替換為更實用的東西。

除了以簡單的方式構建 API(例如,使用單例類)之外,還需要採取幾個步驟來使其更易於維護、更具可擴展性和安全性。 在本系列的最後一篇文章中,我們將介紹:

  • 用MongoDB替換內存數據庫,然後使用Mongoose簡化編碼過程
  • 使用 JWT 以無狀態方法添加安全層和控制訪問
  • 配置自動化測試以允許我們的應用程序擴展

您可以在此處瀏覽本文的最終代碼。