Node.js / TypeScript REST APIの構築、パート2:モデル、ミドルウェア、およびサービス
公開: 2022-03-11REST APIシリーズの最初の記事では、npmを使用してバックエンドを最初から作成する方法、TypeScriptなどの依存関係を追加する方法、Node.jsに組み込まれているdebugモジュールを使用する方法、Express.jsプロジェクト構造を構築する方法、ランタイムをログに記録する方法について説明しました。 Winstonで柔軟にイベントを開催できます。 これらの概念にすでに慣れている場合は、これを複製し、 git checkoutを使用してtoptal-article-01ブランチに切り替えて、読み進めてください。
REST APIサービス、ミドルウェア、コントローラー、およびモデル
約束どおり、これらのモジュールについて詳しく説明します。
- ミドルウェアとコントローラーが呼び出すことができる関数にビジネスロジック操作をカプセル化することにより、コードをよりクリーンにするサービス。
- Express.jsが適切なコントローラー関数を呼び出す前に前提条件を検証するミドルウェア。
- 最終的にリクエスターに応答を送信する前に、サービスを使用して要求を処理するコントローラー。
- データを記述し、コンパイル時のチェックを支援するモデル。
また、本番環境にはまったく適さない非常に基本的なデータベースも追加します。 (その唯一の目的は、このチュートリアルをわかりやすくすることであり、次の記事でデータベース接続とMongoDBおよびMongooseとの統合について詳しく説明するための道を開くことです。)
ハンズオン:DAO、DTO、および一時データベースの最初のステップ
チュートリアルのこの部分では、データベースはファイルを使用しません。 ユーザーデータを配列に保持するだけです。つまり、Node.jsを終了するたびにデータが蒸発します。 最も基本的な作成、読み取り、更新、削除(CRUD)操作のみをサポートします。
ここでは、次の2つの概念を使用します。
- データアクセスオブジェクト(DAO)
- データ転送オブジェクト(DTO)
頭字語間の1文字の違いは不可欠です。DAOは、定義されたデータベースへの接続とCRUD操作の実行を担当します。 DTOは、DAOがデータベースとの間で送受信する生データを保持するオブジェクトです。
つまり、DTOはデータモデルタイプに準拠するオブジェクトであり、DAOはそれらを使用するサービスです。
この記事では、DTOはより複雑になる可能性があります(たとえば、ネストされたデータベースエンティティを表す)が、単一のDTOインスタンスは、単一のデータベース行に対する特定のアクションに対応します。
なぜDTOなのか?
以下のサービスのセクションで説明するように、DTOを使用してTypeScriptオブジェクトをデータモデルに準拠させると、アーキテクチャの一貫性を維持するのに役立ちます。 ただし、重要な注意点があります。DTOもTypeScript自体も、実行時に実行する必要があるため、ユーザー入力の自動検証を約束するものではありません。 コードがAPIのエンドポイントでユーザー入力を受け取ると、その入力は次のようになります。
- 追加のフィールドがあります
- 必須フィールド(つまり、末尾に
?が付いていないフィールド)が欠落している - データがTypeScriptを使用してモデルで指定したタイプではないフィールドがある
TypeScript(およびそれがトランスパイルされるJavaScript)は、これを魔法のようにチェックしないため、特にAPIを公開する場合は、これらの検証を忘れないようにすることが重要です。 ajvのようなパッケージはこれに役立ちますが、通常はネイティブのTypeScriptではなくライブラリ固有のスキーマオブジェクトでモデルを定義することで機能します。 (次の記事で説明するマングースは、このプロジェクトでも同様の役割を果たします。)
「もっと単純なものではなく、DAOとDTOの両方を使用するのが本当に最善ですか?」と考えているかもしれません。 エンタープライズ開発者のGuntherPoppが答えを提供します。 中期的な拡張が合理的に期待できる場合を除いて、ほとんどの小規模な実世界のExpress.js/TypeScriptプロジェクトではDTOを回避することをお勧めします。
ただし、本番環境でそれらを使用する予定がない場合でも、このサンプルプロジェクトは、TypeScriptAPIアーキテクチャを習得するための貴重な機会です。 これは、TypeScriptタイプを追加の方法で活用し、DTOを操作して、コンポーネントやモデルを追加する際のより基本的なアプローチと比較する方法を確認するための優れた方法です。
TypeScriptレベルのユーザーRESTAPIモデル
まず、ユーザー用に3つの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に戻り、 users.dao.tsをインポートします。
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操作を関数としてクラスに追加します。 create関数は次のようになります。
async addUser(user: CreateUserDto) { user.id = shortid.generate(); this.users.push(user); return user.id; }読み取りには、「すべてのリソースを読み取る」と「IDで1つ読み取る」の2種類があります。 これらは次のようにコーディングされています。
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などの既存のライブラリを使用してデータベースに接続する可能性があります。これにより、必要になる可能性のあるすべての基本的な操作が抽象化されます。 このため、上記で実装された関数の詳細については説明しません。
RESTAPIサービスレイヤー
基本的なメモリ内DAOができたので、CRUD関数を呼び出すサービスを作成できます。 CRUD関数は、データベースに接続するすべてのサービスに必要なものであるため、新しいサービスを実装するたびに、実装するメソッドを含むCRUDインターフェイスを作成します。
現在、私たちが使用しているIDEには、実装している関数を追加するためのコード生成機能があり、作成する必要のある繰り返しコードの量を減らすことができます。
WebStormIDEを使用した簡単な例:
IDEは、MyServiceクラス名を強調表示し、次のオプションを提案します。
「すべてのメンバーを実装する」オプションは、 CRUDインターフェースに準拠するために必要な機能を即座に足場にします。
とはいえ、最初にCRUDと呼ばれるTypeScriptインターフェイスを作成しましょう。 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タイプをインポートすることでした。次に、DAOで使用したのと同じパターンであるサービスシングルトンとしてUsersServiceを実装します。
すべてのCRUD関数は、 UsersDaoのそれぞれの関数を呼び出すだけです。 DAOを交換するときは、パート3で説明するように、DAO関数が呼び出されるこのファイルにいくつかの調整を加えることを除いて、プロジェクトの他の場所で変更を加える必要はありません。
たとえば、 list()のすべての呼び出しを追跡し、置き換える前にそのコンテキストを確認する必要はありません。 これは、上記の最初のボイラープレートの量が少ないという犠牲を払って、この分離層を持つことの利点です。
Async/AwaitとNode.js
サービス機能にasyncを使用するのは無意味に思えるかもしれません。 今のところ、それは次のとおりです。これらの関数はすべて、 Promiseやawaitを内部で使用せずに、すぐに値を返します。 これは、 asyncを使用するサービスのコードベースを準備するためだけのものです。 同様に、以下では、これらの関数へのすべての呼び出しがawaitを使用していることがわかります。
この記事の終わりまでに、実験用の実行可能なプロジェクトが再びできあがります。 これは、コードベースのさまざまな場所にさまざまな種類のエラーを追加し、コンパイルとテスト中に何が起こるかを確認するのに最適な瞬間です。 特にasyncコンテキストでのエラーは、期待どおりに動作しない場合があります。 この記事の範囲を超えているさまざまなソリューションを掘り下げて調査する価値があります。
これで、DAOとサービスの準備ができたので、ユーザーコントローラーに戻りましょう。
RESTAPIコントローラーの構築
上で述べたように、コントローラーの背後にある考え方は、ルート構成を、最終的にルート要求を処理するコードから分離することです。 つまり、すべての検証は、リクエストがコントローラーに到達する前に実行する必要があります。 コントローラーは実際のリクエストをどう処理するかを知る必要があるだけです。なぜなら、リクエストがそれまでにそれを行った場合、それが有効であることがわかったからです。 次に、コントローラーは、処理する各要求のそれぞれのサービスを呼び出します。
始める前に、ユーザーパスワードを安全にハッシュするためのライブラリをインストールする必要があります。
npm i argon2 まず、 users controllersフォルダー内に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応答で何も返送しない上記の行は、このトピックに関するRFC7231に準拠しています。
ユーザーコントローラーのシングルトンが完成したら、RESTAPIオブジェクトモデルとサービスの例に依存する他のモジュールであるユーザーミドルウェアをコーディングする準備が整いました。
Express.jsを使用したNode.jsRESTミドルウェア
Express.jsミドルウェアで何ができるでしょうか? 検証は、1つに最適です。 リクエストがユーザーコントローラーに届く前に、リクエストのゲートキーパーとして機能する基本的な検証をいくつか追加しましょう。
- ユーザーを作成または更新するために必要な
emailやpasswordなどのユーザーフィールドが存在することを確認します - 特定のメールがまだ使用されていないことを確認します
- 作成後に
emailフィールドを変更していないことを確認します(簡単にするために、ユーザー向けのプライマリIDとして使用しているため) - 特定のユーザーが存在するかどうかを検証します
これらの検証をExpress.jsで機能させるには、前の記事で説明したように、 next()を使用してフロー制御のExpress.jsパターンに従う関数に変換する必要があります。 新しいファイル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を毎回取得することを心配せずに、全身リクエストを簡単に使用できるようにすることです。 代わりに、ミドルウェアという1つの場所で処理されます。 関数は次のようになります。
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関数を渡して、エンドポイント/users/:userIdでGET 、 PUT 、 PATCH 、またはDELETEが実行される前に呼び出されます。 つまりvalidateUserExistsは、 .put()または.patch() )に渡す追加の関数配列に含まれている必要はありません。そこで指定された関数の前に呼び出されます。
ここでも、ミドルウェア固有の再利用性を別の方法で活用しました。 POSTとPUTの両方のコンテキストで使用されるUsersMiddleware.validateRequiredUserBodyFieldsを渡すことにより、他のミドルウェア関数とエレガントに再結合しています。
免責事項:この記事では、基本的な検証のみを取り上げます。 実際のプロジェクトでは、コーディングに必要なすべての制限について考え、見つける必要があります。 簡単にするために、ユーザーが自分の電子メールを変更できないことも想定しています。
Express / TypeScriptRESTAPIのテスト
これで、Node.jsアプリをコンパイルして実行できます。 実行が完了すると、PostmanやcURLなどのRESTクライアントを使用して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は、新しく作成されたユーザーの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に使用する場合、標準のRESTパターンに準拠する場合は、APIコンシューマーとしてオブジェクト全体を送信する必要があることに注意してください。 つまり、 lastNameフィールドだけを更新したいが、 PUTエンドポイントを使用すると、更新するオブジェクト全体を送信する必要があります。 lastNameフィールドのみを送信するのはまだ標準のREST制約内にあるため、 PATCHリクエストを使用する方が簡単です。
curl --request PATCH "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' \ --data-raw '{ "lastName": "Faraco" }' 私たち自身のコードベースでは、この記事で追加したミドルウェア機能を使用して、 PUTとPATCHのこの区別を強制するのはルート構成であることを思い出してください。
PUT 、 PATCH 、またはその両方?
PATCHの柔軟性を考えると、 PUTをサポートする理由はあまりないように思われるかもしれません。一部のAPIはそのアプローチを採用します。 他の人は、APIを「完全にREST準拠」にするために、 PUTをサポートすることを主張する場合があります。その場合、フィールドごとのPUTルートを作成することは、一般的なユースケースに適した戦術かもしれません。
実際には、これらのポイントは、2つの間の実際の違いから、 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を使用してRESTAPIを構築するための重要な手順についてさらに詳しく説明しました。 コードを分割して、サービス、ミドルウェア、コントローラー、モデルをサポートします。 それぞれの機能には、検証、論理操作、または有効な要求の処理とそれらへの応答など、特定の役割があります。
また、データを保存するための非常に簡単な方法を作成しました。この時点でテストを許可し、シリーズの次のパートでより実用的なものに置き換えるという(駄洒落を許してください)という明確な目的を持っています。
たとえばシングルトンクラスを使用して、単純さを念頭に置いてAPIを構築することに加えて、保守を容易にし、よりスケーラブルで、安全にするために実行するいくつかのステップがあります。 シリーズの最後の記事では、以下について説明します。
- インメモリデータベースをMongoDBに置き換え、Mongooseを使用してコーディングプロセスを簡素化する
- JWTを使用したステートレスアプローチでのセキュリティレイヤーの追加とアクセスの制御
- アプリケーションを拡張できるように自動テストを構成する
この記事から最終的なコードをここで閲覧できます。
