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

目次
- イントロダクション`
- データベースに
User
モデルを追加する - ユーザーのCRUDエンドポイントを実装する
- レスポンスボディから
password
フィールドを除外する - 記事とともに著者情報を返す
- まとめと終わりに
イントロダクション
このシリーズの最初の章では、新しいNestJSプロジェクトを作成し、Prisma、PostgreSQL、Swaggerと統合しました。次に、ブログアプリケーションのバックエンド用の基本的なREST APIを構築しました。第2章では、入力検証と変換の方法を学びました。
この章では、データレイヤーとAPIレイヤーでリレーショナルデータを扱う方法を学びます。
- まず、
User
モデルをデータベーススキーマに追加します。これは、Article
レコードと1対多の関係を持ちます(つまり、1人のユーザーが複数の記事を持つことができます)。 - 次に、
User
レコードに対してCRUD(作成、読み取り、更新、削除)操作を実行するためのUser
エンドポイントのAPIルートを実装します。 - 最後に、APIレイヤーで
User-Article
リレーションをモデル化する方法を学びます。
このチュートリアルでは、第2章で構築したREST APIを使用します。
開発環境
このチュートリアルを進めるには、以下が必要です。
- ... Node.js がインストールされていること。
- ... Docker と Docker 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
ブランチをチェックアウトしてください。
開始するには、次の操作を実行します。
- クローンしたディレクトリに移動します
- 依存関係をインストールします
- DockerでPostgreSQLデータベースを起動します
- データベースマイグレーションを適用します
- プロジェクトを開始します
注意:ステップ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
モデルには、id
、email
、password
など、予想されるいくつかのフィールドがあります。また、Article
モデルとの1対多の関係もあります。これは、1人のユーザーが多数の記事を持つことができるが、1つの記事には1人の作成者しか持つことができないことを意味します。簡単にするために、author
リレーションはオプションになっているため、作成者なしで記事を作成することもできます。
データベースに変更を適用するには、マイグレーションコマンドを実行します。
マイグレーションが正常に実行されると、次の出力が表示されるはずです。
シードスクリプトを更新する
シードスクリプトは、データベースにダミーデータを投入する役割を果たします。シードスクリプトを更新して、データベースにいくつかのユーザーを作成します。
prisma/seed.ts
ファイルを開き、次のように更新します。
シードスクリプトは、2人のユーザーと3つの記事を作成するようになりました。最初の記事は最初のユーザーによって書かれ、2番目の記事は2番目のユーザーによって書かれ、3番目の記事は誰によっても書かれていません。
注意:現時点では、パスワードをプレーンテキストで保存しています。実際のアプリケーションでは絶対にしないでください。次の章では、パスワードのソルト化とハッシュ化について詳しく学びます。
シードスクリプトを実行するには、次のコマンドを実行します。
シードスクリプトが正常に実行されると、次の出力が表示されるはずです。
ArticleEntity
にauthorId
フィールドを追加する
マイグレーションを実行した後、新しい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プロンプトが表示されます。質問に適切に答えてください。
このリソースに使用する名前(複数形、例:「users」)は何ですか?
usersどのトランスポート層を使用しますか?
REST APICRUDエントリポイントを生成しますか?
Yes
これで、src/users
ディレクトリに新しいusers
モジュールがRESTエンドポイントのすべてのボイラープレートとともに表示されるはずです。
src/users/users.controller.ts
ファイル内には、さまざまなルート(ルートハンドラーとも呼ばれます)の定義が表示されます。各リクエストを処理するためのビジネスロジックは、src/users/users.service.ts
ファイルにカプセル化されています。
Swaggerで生成されたAPIページを開くと、次のようなものが表示されるはずです。
Users
モジュールにPrismaClient
を追加する
Users
モジュール内でPrismaClient
にアクセスするには、PrismaModule
をインポートとして追加する必要があります。次のimports
をUsersModule
に追加します。
これで、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
を利用し、リクエストボディを定義するためにCreateUserDto
とUpdateUserDto
を利用します。
コントローラーは、さまざまなルートハンドラーで構成されています。このクラスには、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ページは次のようになります。
さまざまなエンドポイントをテストして、期待どおりに動作することを確認してください。
レスポンスボディからpassword
フィールドを除外する
users
APIは期待どおりに動作しますが、大きなセキュリティ上の欠陥があります。password
フィールドが、さまざまなエンドポイントのレスポンスボディで返されます。
この問題を修正するには、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
ルートハンドラーを更新します。
これで、パスワードはレスポンスオブジェクトから省略されるはずです。
記事とともに著者情報を返す
第1章では、単一の記事を取得するためのGET /articles/:id
エンドポイントを実装しました。現在、このエンドポイントは記事のauthor
を返さず、authorId
のみを返します。author
をフェッチするには、GET /users/:id
エンドポイントに追加のリクエストを行う必要があります。記事とその作成者の両方が必要な場合は、2つのAPIリクエストを行う必要があるため、これは理想的ではありません。この記事とArticle
オブジェクトとともにauthor
を返すことで、これを改善できます。
データアクセスロジックは、ArticlesService
内で実装されています。findOne()
メソッドを更新して、Article
オブジェクトとともにauthor
を返すようにします。
GET /articles/:id
エンドポイントをテストすると、記事の作成者が存在する場合は、レスポンスオブジェクトに含まれていることがわかります。ただし、問題があります。password
フィールドが再び公開されています🤦。
この問題の原因は、前回と非常によく似ています。現在、ArticlesController
はPrismaによって生成された型のインスタンスを返しますが、ClassSerializerInterceptor
はUserEntity
クラスで動作します。これを修正するには、ArticleEntity
クラスの実装を更新し、author
プロパティをUserEntity
のインスタンスで初期化するようにします。
ここでも、Object.assign()
メソッドを使用して、data
オブジェクトからArticleEntity
インスタンスにプロパティをコピーしています。author
プロパティは、存在する場合、UserEntity
のインスタンスとして初期化されます。
次に、ArticleEntity
オブジェクトのインスタンスを返すようにArticlesController
を更新します。
これで、GET /articles/:id
はpassword
フィールドなしでauthor
オブジェクトを返します。
まとめと終わりに
この章では、Prismaを使用してNestJSアプリケーションでリレーショナルデータをモデル化する方法を学びました。また、ClassSerializerInterceptor
と、エンティティクラスを使用してクライアントに返されるデータを制御する方法についても学びました。
このチュートリアルの完成したコードは、end-relational-data
ブランチのGitHubリポジトリにあります。問題に気付いた場合は、リポジトリで問題を提起するか、PRを送信してください。また、Twitterで直接私に連絡することもできます。
次回の投稿をお見逃しなく!
Prismaニュースレターにサインアップ