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

目次
- はじめに`
- データベースに
User
モデルを追加する - ユーザーのCRUDエンドポイントを実装する
- レスポンスボディから
password
フィールドを除外する - 記事とともに著者を返す
- まとめと最後のコメント
はじめに
このシリーズの最初の章では、新しいNestJSプロジェクトを作成し、Prisma、PostgreSQL、Swaggerと統合しました。その後、ブログアプリケーションのバックエンドとして基本的なREST APIを構築しました。第2章では、入力の検証と変換を行う方法を学びました。
この章では、データ層とAPI層でリレーショナルデータを扱う方法を学びます。
- まず、データベーススキーマに
User
モデルを追加します。これはArticle
レコードとの一対多の関係(つまり、一人のユーザーが複数の記事を持つことができる)を持ちます。 - 次に、
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拡張機能がインストールされていること。Prisma VS Code拡張機能は、Prismaの非常に優れたIntelliSenseとシンタックスハイライトを追加します。
- ... 任意で このシリーズで提供されるコマンドを実行するために、Unixシェル(LinuxやmacOSのターミナル/シェルなど)にアクセスできること。
Unixシェルがない場合(例えば、Windowsマシンを使用している場合)でも、チュートリアルを進めることはできますが、シェルコマンドをマシンに合わせて変更する必要があるかもしれません。
リポジトリをクローンする
このチュートリアルの出発点は、このシリーズの第2章の終わりです。NestJSで構築された基本的なREST APIが含まれています。
このチュートリアルの出発点は、GitHubリポジトリのend-validation
ブランチで利用できます。開始するには、リポジトリをクローンし、end-validation
ブランチをチェックアウトしてください。
さて、始めるために以下の操作を行ってください。
- クローンしたディレクトリに移動する
- 依存関係をインストールする
- DockerでPostgreSQLデータベースを起動する
- データベースマイグレーションを適用する
- プロジェクトを開始する
注: ステップ4では、Prisma Clientの生成とデータベースのシードも行われます。
これで、https://: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
モデルとの一対多の関係も持っています。これは、ユーザーが複数の記事を持つことができる一方で、記事は一人の著者しか持てないことを意味します。簡略化のため、author
リレーションはオプションとされており、著者なしで記事を作成することも可能です。
さて、データベースに変更を適用するには、マイグレーションコマンドを実行します。
マイグレーションが正常に実行されると、以下の出力が表示されるはずです。
シードスクリプトを更新する
シードスクリプトは、データベースにダミーデータを投入する役割を担っています。データベースにいくつかのユーザーを作成するために、シードスクリプトを更新します。
prisma/seed.ts
ファイルを開き、次のように更新します。
シードスクリプトは現在、2人のユーザーと3つの記事を作成します。最初の記事は最初のユーザーによって書かれ、2番目の記事は2番目のユーザーによって書かれ、3番目の記事は誰も書いていません。
注: 現時点では、パスワードを平文で保存しています。実際のアプリケーションでは決してこれを行ってはいけません。次の章で、パスワードのソルト化とハッシュ化について詳しく学びます。
シードスクリプトを実行するには、以下のコマンドを実行します。
シードスクリプトが正常に実行されると、以下の出力が表示されるはずです。
ArticleEntity
にauthorId
フィールドを追加する
マイグレーションを実行した後、新しいTypeScriptエラーに気づいたかもしれません。ArticleEntity
クラスはPrismaによって生成されたArticle
型をimplements
しています。Article
型には新しいauthorId
フィールドがありますが、ArticleEntity
クラスにはそのフィールドが定義されていません。TypeScriptはこの型の不一致を認識し、エラーを発生させています。このエラーを修正するには、ArticleEntity
クラスにauthorId
フィールドを追加します。
ArticleEntity
内に新しいauthorId
フィールドを追加します。
JavaScriptのような弱型付け言語では、このような問題を自分で特定して修正する必要があります。TypeScriptのような強型付け言語の大きな利点の1つは、型関連の問題を迅速に検出できることです。
ユーザーのCRUDエンドポイントを実装する
このセクションでは、REST APIに/users
リソースを実装します。これにより、データベース内のユーザーに対してCRUD操作を実行できるようになります。
注: このセクションの内容は、このシリーズの最初の章にある記事モデルのCRUD操作を実装するセクションの内容と類似しています。そのセクションでは、このトピックがより深くカバーされているため、概念的な理解を深めるために読むことができます。
新しいusers
RESTリソースを生成する
users
の新しいRESTリソースを生成するには、以下のコマンドを実行します。
いくつかCLIのプロンプトが表示されます。質問に適切に回答してください。
What name would you like to use for this resource (plural, e.g., "users")?
usersWhat transport layer do you use?
REST APIWould you like to generate CRUD entry points?
Yes
これで、src/users
ディレクトリに新しいusers
モジュールが見つかるはずです。そこには、RESTエンドポイントのすべてのボイラープレートが含まれています。
src/users/users.controller.ts
ファイル内には、さまざまなルート(ルートハンドラとも呼ばれます)の定義が表示されます。各リクエストを処理するためのビジネスロジックは、src/users/users.service.ts
ファイルにカプセル化されています。
Swaggerが生成したAPIページを開くと、以下のようなものが表示されるはずです。
Users
モジュールにPrismaClient
を追加する
Users
モジュール内でPrismaClient
にアクセスするには、PrismaModule
をインポートとして追加する必要があります。UsersModule
に以下のimports
を追加します。
これで、UsersService
内でPrismaService
を注入し、それを使用してデータベースにアクセスできます。これを行うには、users.service.ts
に次のようにコンストラクタを追加します。
User
エンティティとDTOクラスを定義する
ArticleEntity
と同様に、API層でUser
エンティティを表すために使用されるUserEntity
クラスを定義します。user.entity.ts
ファイルにUserEntity
クラスを次のように定義します。
@ApiProperty
デコレータは、プロパティをSwaggerに表示するために使用されます。password
フィールドに@ApiProperty
デコレータを追加しなかったことに注意してください。これは、このフィールドが機密情報であり、APIで公開したくないためです。
注:
@ApiProperty
デコレータを省略すると、password
プロパティはSwaggerドキュメントからのみ隠されます。プロパティはレスポンスボディには引き続き表示されます。この問題は後のセクションで扱います。
DTO(データ転送オブジェクト)は、ネットワーク上でデータがどのように送信されるかを定義するオブジェクトです。ユーザーの作成時と更新時に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
デコレータを使用して、各エンドポイントのレスポンスボディを定義します。
更新されたSwaggerのAPIページは次のようになるはずです。
期待どおりに動作することを確認するために、さまざまなエンドポイントを自由にテストしてください。
レスポンスボディから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
クラスで定義されたプロパティの任意のサブセットを含むことができることを意味します。
次に、UsersController
のルートハンドラを更新して、Prisma.User
オブジェクトの代わりにUserEntity
を返すようにします。
これで、パスワードはレスポンスオブジェクトから省略されるはずです。
記事とともに著者を返す
第1章では、単一の記事を取得するためのGET /articles/:id
エンドポイントを実装しました。現在、このエンドポイントは記事の著者
を返さず、authorId
のみを返します。著者
を取得するためには、GET /users/:id
エンドポイントに追加のリクエストを行う必要があります。記事とその著者の両方が必要な場合、2つのAPIリクエストを行う必要があるため、これは理想的ではありません。Article
オブジェクトとともに著者
を返すことで、これを改善できます。
データアクセスロジックはArticlesService
内に実装されています。findOne()
メソッドを更新して、Article
オブジェクトとともに著者
を返すようにします。
GET /articles/:id
エンドポイントをテストすると、記事の著者が存在する場合、レスポンスオブジェクトに含まれていることに気づくでしょう。しかし、問題があります。password
フィールドが再び公開されています🤦。
この問題の原因は前回と非常に似ています。現在、ArticlesController
はPrismaによって生成された型のインスタンスを返しますが、ClassSerializerInterceptor
はUserEntity
クラスで機能します。これを修正するには、ArticleEntity
クラスの実装を更新し、author
プロパティをUserEntity
のインスタンスで初期化するようにします。
再び、Object.assign()
メソッドを使用して、data
オブジェクトからArticleEntity
インスタンスにプロパティをコピーしています。author
プロパティが存在する場合は、UserEntity
のインスタンスとして初期化されます。
さて、ArticlesController
を更新して、ArticleEntity
オブジェクトのインスタンスを返すようにします。
これで、GET /articles/:id
はpassword
フィールドなしでauthor
オブジェクトを返します。
まとめと最後のコメント
この章では、Prismaを使用してNestJSアプリケーションでリレーショナルデータをモデル化する方法を学びました。また、ClassSerializerInterceptor
と、エンティティクラスを使用してクライアントに返されるデータを制御する方法についても学びました。
このチュートリアルの完成したコードは、GitHubリポジトリのend-relational-data
ブランチにあります。問題を発見した場合は、遠慮なくリポジトリでIssueを立てるか、PRを送信してください。Twitterで私に直接連絡することもできます。
次の投稿をお見逃しなく!
Prismaニュースレターに登録する