2023年3月23日

NestJSとPrismaでREST APIを構築する:リレーショナルデータの扱い

8分で読めます

NestJS、Prisma、PostgreSQLでREST APIを構築するシリーズの4回目のチュートリアルへようこそ!このチュートリアルでは、NestJS REST APIでリレーショナルデータを扱う方法を学びます。

Building a REST API with NestJS and Prisma: Handling Relational Data

目次

イントロダクション

このシリーズの最初の章では、新しいNestJSプロジェクトを作成し、Prisma、PostgreSQL、Swaggerと統合しました。次に、ブログアプリケーションのバックエンド用の基本的なREST APIを構築しました。第2章では、入力検証と変換の方法を学びました。

この章では、データレイヤーとAPIレイヤーでリレーショナルデータを扱う方法を学びます。

  1. まず、Userモデルをデータベーススキーマに追加します。これは、Articleレコードと1対多の関係を持ちます(つまり、1人のユーザーが複数の記事を持つことができます)。
  2. 次に、Userレコードに対してCRUD(作成、読み取り、更新、削除)操作を実行するためのUserエンドポイントのAPIルートを実装します。
  3. 最後に、APIレイヤーでUser-Articleリレーションをモデル化する方法を学びます。

このチュートリアルでは、第2章で構築したREST APIを使用します。

開発環境

このチュートリアルを進めるには、以下が必要です。

  • ... Node.js がインストールされていること。
  • ... DockerDocker Compose がインストールされていること。Linuxを使用している場合は、Dockerバージョンが20.10.0以上であることを確認してください。ターミナルでdocker versionを実行してDockerバージョンを確認できます。
  • ... オプションで Prisma VS Code Extension がインストールされていること。Prisma VS Code Extensionは、Prismaに非常に優れたIntelliSenseと構文強調表示を追加します。
  • ... オプションで このシリーズで提供されるコマンドを実行するために、Unixシェル(LinuxおよびmacOSのターミナル/シェルなど)にアクセスできること。

Unixシェルがない場合(たとえば、Windowsマシンを使用している場合)でも、このチュートリアルを進めることはできますが、シェルコマンドをマシンに合わせて変更する必要がある場合があります。

リポジトリのクローン

このチュートリアルの開始点は、このシリーズの第2章の終わりです。これには、NestJSで構築された基本的なREST APIが含まれています。

このチュートリアルの開始点は、end-validationブランチのGitHubリポジトリにあります。開始するには、リポジトリをクローンしてend-validationブランチをチェックアウトしてください。

開始するには、次の操作を実行します。

  1. クローンしたディレクトリに移動します
  1. 依存関係をインストールします
  1. DockerでPostgreSQLデータベースを起動します
  1. データベースマイグレーションを適用します
  1. プロジェクトを開始します

注意:ステップ4では、Prisma Clientの生成とデータベースのシードも行われます。

これで、http://localhost:3000/api/でAPIドキュメントにアクセスできるようになります。

プロジェクトの構成とファイル

クローンしたリポジトリは、次の構成になっているはずです。

注意:このフォルダーにはtestディレクトリも付属していることに気付くかもしれません。テストはこのチュートリアルでは扱いません。ただし、Prismaを使用したアプリケーションのテストのベストプラクティスについて学びたい場合は、このチュートリアルシリーズPrismaを使用した究極のテストガイドを必ず確認してください。

このリポジトリの注目すべきファイルとディレクトリは次のとおりです。

  • srcディレクトリには、アプリケーションのソースコードが含まれています。3つのモジュールがあります。
    • appモジュールはsrcディレクトリのルートにあり、アプリケーションのエントリーポイントです。Webサーバーの起動を担当します。
    • prismaモジュールには、データベースへのインターフェースであるPrisma Clientが含まれています。
    • articlesモジュールは、/articlesルートとそれに付随するビジネスロジックのエンドポイントを定義します。
  • prismaフォルダーには、次のものが含まれています。
    • schema.prismaファイルは、データベーススキーマを定義します。
    • migrationsディレクトリには、データベースマイグレーション履歴が含まれています。
    • seed.tsファイルには、開発データベースにダミーデータをシードするスクリプトが含まれています。
  • docker-compose.ymlファイルは、PostgreSQLデータベースのDockerイメージを定義します。
  • .envファイルには、PostgreSQLデータベースのデータベース接続文字列が含まれています。

注意:これらのコンポーネントの詳細については、このチュートリアルシリーズの第1章を参照してください。

データベースにUserモデルを追加する

現在、データベーススキーマにはArticleという単一のモデルしかありません。記事は登録ユーザーによって書くことができます。したがって、この関係を反映するために、データベーススキーマにUserモデルを追加します。

まず、Prismaスキーマを更新します。

Userモデルには、idemailpasswordなど、予想されるいくつかのフィールドがあります。また、Articleモデルとの1対多の関係もあります。これは、1人のユーザーが多数の記事を持つことができるが、1つの記事には1人の作成者しか持つことができないことを意味します。簡単にするために、authorリレーションはオプションになっているため、作成者なしで記事を作成することもできます。

データベースに変更を適用するには、マイグレーションコマンドを実行します。

マイグレーションが正常に実行されると、次の出力が表示されるはずです。

シードスクリプトを更新する

シードスクリプトは、データベースにダミーデータを投入する役割を果たします。シードスクリプトを更新して、データベースにいくつかのユーザーを作成します。

prisma/seed.tsファイルを開き、次のように更新します。

シードスクリプトは、2人のユーザーと3つの記事を作成するようになりました。最初の記事は最初のユーザーによって書かれ、2番目の記事は2番目のユーザーによって書かれ、3番目の記事は誰によっても書かれていません。

注意:現時点では、パスワードをプレーンテキストで保存しています。実際のアプリケーションでは絶対にしないでください。次の章では、パスワードのソルト化とハッシュ化について詳しく学びます。

シードスクリプトを実行するには、次のコマンドを実行します。

シードスクリプトが正常に実行されると、次の出力が表示されるはずです。

ArticleEntityauthorIdフィールドを追加する

マイグレーションを実行した後、新しいTypeScriptエラーに気付いたかもしれません。ArticleEntityクラスは、Prismaによって生成されたArticle型をimplementsしています。Article型には新しいauthorIdフィールドがありますが、ArticleEntityクラスにはそのフィールドが定義されていません。TypeScriptは、型に不一致があることを認識し、エラーを発生させています。authorIdフィールドをArticleEntityクラスに追加して、このエラーを修正します。

ArticleEntity内で、新しいauthorIdフィールドを追加します。

JavaScriptのような弱型付け言語では、このようなものを自分で特定して修正する必要があります。TypeScriptのような強型付け言語を使用することの大きな利点の1つは、型関連の問題を迅速に検出できることです。

ユーザーのCRUDエンドポイントを実装する

このセクションでは、REST APIで/usersリソースを実装します。これにより、データベース内のユーザーに対してCRUD操作を実行できるようになります。

注意:このセクションの内容は、このシリーズの最初の章の記事モデルのCRUD操作の実装セクションの内容と似ています。そのセクションでは、トピックについてより詳細に説明しているので、概念的な理解を深めるために読むことができます。

新しいusers RESTリソースを生成する

usersの新しいRESTリソースを生成するには、次のコマンドを実行します。

いくつかのCLIプロンプトが表示されます。質問に適切に答えてください。

  1. このリソースに使用する名前(複数形、例:「users」)は何ですか? users
  2. どのトランスポート層を使用しますか? REST API
  3. CRUDエントリポイントを生成しますか? Yes

これで、src/usersディレクトリに新しいusersモジュールがRESTエンドポイントのすべてのボイラープレートとともに表示されるはずです。

src/users/users.controller.tsファイル内には、さまざまなルート(ルートハンドラーとも呼ばれます)の定義が表示されます。各リクエストを処理するためのビジネスロジックは、src/users/users.service.tsファイルにカプセル化されています。

Swaggerで生成されたAPIページを開くと、次のようなものが表示されるはずです。

Auto-generated "users" endpoints

UsersモジュールにPrismaClientを追加する

Usersモジュール内でPrismaClientにアクセスするには、PrismaModuleをインポートとして追加する必要があります。次のimportsUsersModuleに追加します。

これで、UsersService内でPrismaServiceを注入し、それを使用してデータベースにアクセスできます。これを行うには、次のようにusers.service.tsにコンストラクターを追加します。

UserエンティティとDTOクラスを定義する

ArticleEntityと同様に、APIレイヤーでUserエンティティを表すために使用されるUserEntityクラスを定義します。user.entity.tsファイルにUserEntityクラスを次のように定義します。

@ApiPropertyデコレーターは、プロパティをSwaggerに表示するために使用されます。passwordフィールドに@ApiPropertyデコレーターを追加していないことに注意してください。これは、このフィールドが機密情報であり、APIで公開したくないためです。

注意@ApiPropertyデコレーターを省略すると、passwordプロパティはSwaggerドキュメントからのみ非表示になります。プロパティはレスポンスボディに引き続き表示されます。この問題は後のセクションで処理します。

DTO(Data Transfer Object)は、ネットワーク経由でデータがどのように送信されるかを定義するオブジェクトです。ユーザーの作成時と更新時にAPIに送信されるデータを定義するために、CreateUserDtoクラスとUpdateUserDtoクラスを実装する必要があります。create-user.dto.tsファイル内にCreateUserDtoクラスを次のように定義します。

@IsString@MinLength、および@IsNotEmptyは、APIに送信されるデータを検証するために使用される検証デコレーターです。検証の詳細については、このシリーズの第2章で説明しています。

UpdateUserDtoの定義はCreateUserDtoの定義から自動的に推測されるため、明示的に定義する必要はありません。

UsersServiceクラスを定義する

UsersServiceは、Prisma Clientを使用してデータベースからデータを変更およびフェッチし、UsersControllerに提供する役割を果たします。このクラスでcreate()findAll()findOne()update()、およびremove()メソッドを実装します。

UsersControllerクラスを定義する

UsersControllerは、usersエンドポイントへのリクエストとレスポンスを処理する役割を果たします。データベースにアクセスするためにUsersServiceを利用し、レスポンスボディを定義するためにUserEntityを利用し、リクエストボディを定義するためにCreateUserDtoUpdateUserDtoを利用します。

コントローラーは、さまざまなルートハンドラーで構成されています。このクラスには、5つのエンドポイントに対応する5つのルートハンドラーを実装します。

  • create() - POST /users
  • findAll() - GET /users
  • findOne() - GET /users/:id
  • update() - PATCH /users/:id
  • remove() - DELETE /users/:id

users.controller.tsでこれらのルートハンドラーの実装を次のように更新します。

更新されたコントローラーは、@ApiTagsデコレーターを使用してエンドポイントをusersタグの下にグループ化します。また、@ApiCreatedResponseおよび@ApiOkResponseデコレーターを使用して、各エンドポイントのレスポンスボディを定義します。

更新されたSwaggerAPIページは次のようになります。

Updated swagger page

さまざまなエンドポイントをテストして、期待どおりに動作することを確認してください。

レスポンスボディからpasswordフィールドを除外する

users APIは期待どおりに動作しますが、大きなセキュリティ上の欠陥があります。passwordフィールドが、さまざまなエンドポイントのレスポンスボディで返されます。

GET /users/:id reveals password

この問題を修正するには、2つのオプションがあります。

  1. コントローラーのルートハンドラーでレスポンスボディからパスワードを手動で削除する
  2. インターセプターを使用して、レスポンスボディからパスワードを自動的に削除する

最初のオプションはエラーが発生しやすく、不要なコードの重複が発生します。したがって、2番目の方法を使用します。

ClassSerializerInterceptorを使用してレスポンスからフィールドを削除する

NestJSのインターセプターを使用すると、リクエスト-レスポンスサイクルにフックでき、ルートハンドラーの実行前と実行後に追加のロジックを実行できます。この場合、それを使用してレスポンスボディからpasswordフィールドを削除します。

NestJSには、オブジェクトの変換に使用できる組み込みのClassSerializerInterceptorがあります。このインターセプターを使用して、レスポンスオブジェクトからpasswordフィールドを削除します。

まず、main.tsを更新して、ClassSerializerInterceptorをグローバルに有効にします。

注意:インターセプターは、グローバルではなく、メソッドまたはコントローラーにバインドすることもできます。詳細については、NestJSドキュメントを参照してください。

ClassSerializerInterceptorは、オブジェクトの変換方法を定義するためにclass-transformerパッケージを使用します。@Exclude()デコレーターを使用して、UserEntityクラスのpasswordフィールドを除外します。

GET /users/:idエンドポイントを再度使用しようとすると、passwordフィールドがまだ公開されていることに気付くでしょう🤔。これは、現在、コントローラーのルートハンドラーがPrisma Clientによって生成されたUser型を返しているためです。ClassSerializerInterceptorは、@Exclude()デコレーターで装飾されたクラスでのみ機能します。この場合、UserEntityクラスです。したがって、ルートハンドラーを更新して、代わりにUserEntity型を返す必要があります。

まず、UserEntityオブジェクトをインスタンス化するコンストラクターを作成する必要があります。

コンストラクターはオブジェクトを受け取り、Object.assign()メソッドを使用して、partialオブジェクトからUserEntityインスタンスにプロパティをコピーします。partialの型はPartial<UserEntity>です。これは、partialオブジェクトにUserEntityクラスで定義されたプロパティの任意のサブセットを含めることができることを意味します。

次に、Prisma.Userオブジェクトではなく、UserEntityを返すようにUsersControllerルートハンドラーを更新します。

これで、パスワードはレスポンスオブジェクトから省略されるはずです。

GET /users/:id does not reveal password

記事とともに著者情報を返す

第1章では、単一の記事を取得するためのGET /articles/:idエンドポイントを実装しました。現在、このエンドポイントは記事のauthorを返さず、authorIdのみを返します。authorをフェッチするには、GET /users/:idエンドポイントに追加のリクエストを行う必要があります。記事とその作成者の両方が必要な場合は、2つのAPIリクエストを行う必要があるため、これは理想的ではありません。この記事とArticleオブジェクトとともにauthorを返すことで、これを改善できます。

データアクセスロジックは、ArticlesService内で実装されています。findOne()メソッドを更新して、Articleオブジェクトとともにauthorを返すようにします。

GET /articles/:idエンドポイントをテストすると、記事の作成者が存在する場合は、レスポンスオブジェクトに含まれていることがわかります。ただし、問題があります。passwordフィールドが再び公開されています🤦。

GET /articles/:id reveals password

この問題の原因は、前回と非常によく似ています。現在、ArticlesControllerはPrismaによって生成された型のインスタンスを返しますが、ClassSerializerInterceptorUserEntityクラスで動作します。これを修正するには、ArticleEntityクラスの実装を更新し、authorプロパティをUserEntityのインスタンスで初期化するようにします。

ここでも、Object.assign()メソッドを使用して、dataオブジェクトからArticleEntityインスタンスにプロパティをコピーしています。authorプロパティは、存在する場合、UserEntityのインスタンスとして初期化されます。

次に、ArticleEntityオブジェクトのインスタンスを返すようにArticlesControllerを更新します。

これで、GET /articles/:idpasswordフィールドなしでauthorオブジェクトを返します。

GET /articles/:id does not reveal password

まとめと終わりに

この章では、Prismaを使用してNestJSアプリケーションでリレーショナルデータをモデル化する方法を学びました。また、ClassSerializerInterceptorと、エンティティクラスを使用してクライアントに返されるデータを制御する方法についても学びました。

このチュートリアルの完成したコードは、end-relational-dataブランチのGitHubリポジトリにあります。問題に気付いた場合は、リポジトリで問題を提起するか、PRを送信してください。また、Twitterで直接私に連絡することもできます。

次回の投稿をお見逃しなく!

Prismaニュースレターにサインアップ