Nest.js入門
学習用メモ
- バリデーションと例外処理
- DTO
- バリデーション
- Class Validator
- Pipes
- Get uuidに対するバリデーションはいつ行うべきか
- ParseUUIDPipeか、@IsUUID
- 例外処理
- Logger
- テスト
-
ストアドプロシージャー
- 略:ストアド、プロシージャー、sp
- 一度のDB接続でOK
- SQLインジェクション
- typedSQL
- Custom providers
リクエストからレスポンスまでの順序
ざっくり要約
- クライアントがリクエストを送信。
- コントローラーがリクエストを受け取り、ユースケースに処理を依頼。
- ユースケースが、リポジトリインターフェースを介してデータ取得をリポジトリに依頼。
- リポジトリは、データベースからデータを取得し、モデルにマッピング。
- モデルがユースケースにデータを返し、ユースケースがそのデータをコントローラーに返す。
- コントローラーがレスポンスをクライアントに返す。
1. クライアントからリクエスト
- クライアント(ブラウザ、Postman、フロントエンドアプリなど)からHTTPリクエストを送信します。
- 例:
GET /services/123
のようなリクエスト
2.コントローラー(Controller)
- リクエストがNext.jsのアプリケーションに到達すると、対応するコントローラーがそのリクエストを受け取ります。
- コントローラーは、リクエストの内容(パラメータやボディ)を読み取り、必要な処理をユースケース(またはリポジトリ層)に依頼します。
-
ServiceController
は、ServiceUsecase
へリクエストを転送
-
- 例:
ServiceController
のgetServiceById()
メソッドがserviceId
をパラメータとして受け取ります
⭐️メモ:確認↓
@Get(':id')
async getServiceById(@Param('id') id: string) {
return this.serviceUsecase.findById(id);
}
3.ユースケース(Usecase)
-
コントローラーからのリクエストを受け取ったユースケースは、ビジネスロジックを実行します。
-
ユースケースは、
IServiceRepository
インターフェースを介して、データベースにアクセスするリポジトリ層にデータ取得を依頼します。- これにより、データベース操作をビジネスロジックから切り離し、ユースケースがリポジトリの具体的な実装に依存しないようにします。
-
例:
ServiceUsecase
のfindById()
メソッドがリポジトリを使ってサービス情報を取得します。
export class ServicesUsecase {
private readonly serviceRepository: IServiceRepository;
constructor(serviceRepository: IServiceRepository){
this.serviceRepository = serviceRepository;
}
findById = (id: string): Promise<Service> => {
return this.serviceRepository.findById(id);
}
}
4.リポジトリ(Repository)
- ユースケースからの依頼を受けたリポジトリは、データベースにアクセスして必要なデータを取得します。リポジトリは
IServiceRepository
インターフェースを実装しているため、ユースケースはリポジトリの具体的な実装に依存せずに、リポジトリを利用できます。 -
ServicesRepository
はIServiceRepository
を実装し、findById
メソッドを提供します。 - リポジトリは、データベースから取得した情報をモデルにマッピングしてユースケースに返します。
⭐️メモ:確認↓
export class ServicesRepository implements IServiceRepository {
constructor(private prisma: PrismaConnect) {}
async findById(id: string): Promise<Service> {
const data = await this.prisma.service.findUnique({
select: { /* ...フィールド選択... */ }
where: { serviceId: id },
});
return new Service(/* データをマッピング */);
}
}
IServiceRepository
インターフェース
5.-
IServiceRepository
は、リポジトリに要求される処理(例:findById
)に定義した抽象化された契約です。ユースケースは、このインターフェースを通してリポジトリとやりとりするため、リポジトリの実装に依存しません。
export interface IServiceRepository {
findById(id: string): Promise<Service>;
}
6.モデル(Model)
- リポジトリがデータベースから取得したデータは、
Service
というモデルにマッピングされます。このモデルはアプリケーションの中でサービスのデータを表すクラスです。 - リポジトリはデータベースから取得した生データを、
Service
モデルに変換し、ユースケースに返します。これにより、ビジネスロジックやコンロとローラーはオブジェクト形式でデータを扱えるようになります。
⭐️メモ:確認↓
const service: Service = new Service(
data.serviceId,
data.userId,
data.title,
data.catchCopy,
// 省略
);
7.ユースケースに戻る
- リポジトリから取得したデータを受け取ったユースケースは、それをそのままコントローラーに返します。
- ビジネスうロジックに基づいた追加の処理が必要な場合は、ここで実行します。(例えば、フィルタリングや追加ロジック)
8.コントローラーからレスポンスを返す
- コントローラーは、ユースケースから返されたデータを受け取り、それをHTTPレスポンスとしてクライアントに返します。
- データはモデルとして整理されているため、クライアントに適切な形で返されます。
⭐️メモ:確認↓
@Get(':id')
async getServiceById(@Param('id') id: string) {
return this.serviceUsecase.findById(id);
}
9. クライアントにレスポンスが届く
- 最終的に、クライアントはリクエストに対するレスポンスを受け取ります。
- レスポンスは通常、JSON形式でデータを提供します。
Nest.js でよく使われるデコレータ
カテゴリ | デコレータ |
---|---|
クラスに付与するデコレータ |
@Controller() , @Injectable() , @Module() , @Inject()
|
HTTPメソッドに関連するデコレータ |
@Get() , @Post() , @Put() , @Delete() , @Patch()
|
リクエストのデータを取得するデコレータ |
@Param() , @Query() , @Body() , @Headers() , @Req() , @Res()
|
ガードやミドルウェアに関連するデコレータ |
@UseGuards() , @UseInterceptors()
|
その他のデコレータ |
@HttpCode() , @UsePipes()
|
1. クラスに付与するデコレータ
@Controller()
役割
- このクラスがコントローラーであることを示し、特定のルートパスに関連づけます。
使用例
@Controller('users')
export class UsersController {
// /users に対応するエンドポイント
}
詳細
- 上記の場合、引数に設定されている
/users
というルートパスになり、コントローラー内の全てのエンドポイントが自動的に/users
から始まるパスにマッピングされます。
@Injectable()
役割
- このクラスがNest.jsの依存性注入システムによって管理されるプロバイダー(サービス、リポジトリ、ファクトリなど)であることを示します。
使用例
@Injectable()
export class UsersService {
// ビジネスロジックを実装
}
詳細
-
@Injectable()
を使うと、このクラスを他のクラスに依存性としてちゅうにゅうできるようになります。サービスやリポジトリなど、ビジネスロジックやデータアクセスを担当するクラスに付与されます。
@Module()
役割
- クラスをNest.jsモジュールとして定義します。モジュールはアプリケーションの依存関係を管理するために使われます。
@Inject()
役割
- 依存性を手動で注入したい場合に使用します。カスタムプロバイダや特定のインスタンスを注入する際に便利です。
使用例
constructor(@Inject('CUSTOM_TOKEN') private readonly customService: CustomService) {}
2. HTTPメソッドに関連するデコレータ
@Get() , @Post() , @Put() , @Delete() , @Patch()
役割
- コントローラー内で、HTTPリクエストのメソッドを指定します。GET、POST、PUT、DELETE、PATCHなどのリクエストを処理するメソッドを定義する際に使います。
使用例
@Get(':id')
getUser(@Param('id') id: string) {
// GETリクエストを処理
}
@Post()
createUser(@Body() createUserDto: CreateUserDto) {
// POSTリクエストを処理
}
詳細
-
@Get()
:リソースを取得するためのリクエスト(例:/users
) -
@Post()
:新しいリソースを作成するリクエスト(例:/uses
に新しいユーザーを作成) -
@Put()
:リソースを完全に更新するリクエスト(例:/users/1
のユーザーを更新) -
@Delete()
:リソースを削除するリクエスト(例:/users/1
のユーザーを削除) -
@Patch()
:リソースを部分的に更新するリクエスト(例:/users/1
のユーザー情報の一部を更新)
3. リクエストのデータを取得するデコレータ
@Param()
役割
- リクエストのURLパラメータを取得します。
使用例
@Get(':id')
getUser(@Param('id') id: string) {
return this.userService.findById(id);
}
詳細
-
@Param('id')
は、リクエストURL内の:id
の値を取得します。エンドポイントの動的パスパラメータから値を取得したい場合に使います。
@Query()
役割
- クエリパラメータを取得します。例えば、
GET /users?sort=asc
のようなリクエストからsort
の値を取得します。
使用例
@Get()
getUsers(@Query('sort') sort: string) {
return this.userService.getAll(sort);
}
詳細
- リクエストのペイロードからデータを取得します。例えば、フォームデータやJSONデータを処理するときに便利です。
@Headers()
役割
- リクエストヘッダーの値を取得します。
使用例
@Get()
getUserAgent(@Headers('user-agent') userAgent: string) {
return userAgent;
}
詳細
- リクエストヘッダーから特定の値(例えば、Authorization ヘッダーや User-Agent ヘッダー)を取得するために使用します。
@Req()
と @Res()
役割
- リクエストやレスポンスオブジェクトに直接アクセスします。@Req() はリクエストオブジェクト、@Res() はレスポンスオブジェクトを取得します。
使用例
@Get()
getRequestData(@Req() req: Request) {
console.log(req);
}
詳細
- ExpressやFastifyのリクエスト・レスポンスオブジェクトに直接アクセスしたい場合に使います。標準的なNest.jsの方法を使わず、低レベルの操作を行いたいときに便利です。
4.ガードやミドルウェアに関連するデコレータ
@UseGuards()
役割
- 認証や認可などのガードを適用します。
使用例
@UseGuards(AuthGuard('jwt'))
@Get('profile')
getProfile(@Req() req) {
return req.user;
}
詳細
-
AuthGuard
やカスタムガードを使って、特定のエンドポイントにアクセスする際の認証や認可を行います。
@UseInterceptors()
役割
- リクエストやレスポンスに対して、事前・事後処理を行うインターセプターを適用します。
使用例
@UseInterceptors(LoggingInterceptor)
@Get('data')
getData() {
return { message: 'Hello, world!' };
}
詳細
- インターセプターを使用して、リクエストやレスポンスのロギングやレスポンスのフォーマット変更などを行います。
5.その他のよく使うデコレータ
@HttpCode()
役割
- 認証や認可などのガードを適用します。
使用例
@HttpCode(204)
@Delete(':id')
deleteUser(@Param('id') id: string) {
return this.userService.delete(id);
}
詳細
- 通常のHTTPメソッドに応じたデフォルトのステータスコード(200, 201など)を上書きしたい場合に使用します。
@UsePipes()
役割
- バリデーションや変換などのパイプを適用します。
使用例
@UsePipes(ValidationPipe)
@Post()
createUser(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
詳細
- リクエストデータのバリデーションや型変換を行う際に使います。例えば、DTO(Data Transfer Object)を使ってバリデーションを行う場合に便利です。
モデル
モデルは「リクエストからレスポンスまでの順序」で記載の通り、
「モデルがユースケースにデータを返し、ユースケースがそのデータをコントローラーに返す」
という役割を果たしています。
ただ、これではモデルの説明が足りていないので補足します。
しかし、クリーンアーキテクチャにおけるモデルの役割はそれだけではありません。
モデルはドメインオブジェクトとして振る舞い、ビジネスロジックやドメインルールを具現化する重要な部分です。
特にDDD(ドメイン駆動設計)では、モデルがシステムの中心に位置し、ビジネスルールを反映するため、まずモデルを設計することが推奨されます。
モデルは、データベースからとってきたデータをクライアントが求めてるレスポンスのデータ形式に変える必要があるので、作成時は、どんなデータが必要か、レスポンスの形は適切か意識する必要があります。
Nest.jsでのDTOの使い方を初心者向けに解説
まず、Nest.jsは、Node.js向けの効率的で拡張性の高いサーバーサイドアプリケーションフレームワークです。
DTOは、一般的なデザインパターンであり、さまざまなプログラミング言語やフレームワークで使われています。
Nest.jsでは、DTOが特にAPI開発でよく使用されます。
コントローラーに送られてくるデータをDTOで受け取り、バリデーションを行い、ユースケースに渡すという使い方が一般的です。
この記事では、DTOの基本概念とNest.jsでの活用方法を解説します。
(個人的に視覚的に理解したいタイプなので図解も作りました!)
目次
DTOとは何か
DTO(Data Transfer Object)は、アプリケーション間やレイヤー間でデータを転送するためのオブジェクトです。主に以下の目的で使用されます:
- データの構造を定義する
- 不要なデータの露出を防ぐ
- データのバリデーションを容易にする
DTOの役割
-
データ転送用:
DTOは、異なるレイヤー間(例えば、クライアントからサーバー、コントローラーからサービス、サービスからデータベースなど)でデータを転送するためのオブジェクトです。通常、HTTPリクエストやレスポンス、API呼び出しの際に使用されます。 -
データの整形・制限:
DTOは必要なデータのみを含みます。例えば、クライアントから送信されたデータを受け取る際、不要なデータ(パスワードや内部IDなど)は含まれないようにすることができます。 -
バリデーションに使用:
DTOには、class-validator
などを使ってバリデーションルールを定義することができます。これにより、受信したデータが正しいかどうかを検証するために使われます。
Nest.jsでのDTOの定義方法
Nest.jsでは、クラスを用いてDTOを定義します。また、class-validator
とclass-transformer
パッケージを使用して、バリデーションとデータの変換を行います。
必要なパッケージのインストール
npm install class-validator class-transformer
DTOの作成
例えば、ユーザー登録用のDTOを作成してみましょう。
class-validator
とclass-transformer
を組み合わせてバリデーションやデータ変換を行います。
// create-user.dto.ts
import { IsString, IsEmail, IsNotEmpty, MinLength } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(6)
password: string;
@IsString()
@IsNotEmpty()
name: string;
}
DTOの使用例
コントローラーでの使用
// users.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { CreateUserDto } from './create-user.dto';
@Controller('users')
export class UsersController {
@Post()
async create(@Body() createUserDto: CreateUserDto) {
// サービスにデータを渡す
return 'User created';
}
}
バリデーションパイプの適用
Nest.jsでは、グローバルにバリデーションパイプを適用することで、DTOに対するバリデーションを自動的に行えます。
// main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
データフローを図解
以下の図は、クライアントからサーバーへのリクエストと、DTOを通じてサービスまでデータが渡る流れを示しています。
以下を想定。
- フレームワーク: Nest.js
- 言語: TypeScript
- ORM: Prisma
- バリデーションライブラリ: class-validator, class-transformer
- アーキテクチャ: クリーンアーキテクチャ
- 設計パターン: リポジトリパターン
- ドメイン駆動設計(DDD): ドメインオブジェクトやユースケースを使用
- データ転送オブジェクト(DTO): クライアントからのデータバリデーションとデータ構造管理
まとめ
DTOは、データの受け渡しやバリデーションを効率的に行うための強力なツールです。
Nest.jsでは、クラスとデコレーターを用いて簡単にDTOを定義し、バリデーションを実装できます。