Next.jsで class‑validator による型安全性を高める

Next.jsで class‑validator による型安全性を高める

D
dongAuthor
3 min read

Next.jsで開発する際、APIルートやフォームから送られてくるデータの妥当性をどう検証していますか?ExpressやNestJSのようなフレームワークとは異なり、Next.jsには組み込みのバリデーション層がありません。このため、開発者が自らデータの整合性をチェックするコードを書く必要があります。

この投稿では、NestJS開発者には馴染みのある class‑validator ライブラリをNext.jsプロジェクトに適用する方法をご紹介します。デコレーター基盤の綺麗なバリデーションロジックを通じて、いかにしてより安定的で保守性の高いコードが書けるかをステップバイステップで見ていきます。

実際、私自身の経験上、 class‑validator を導入して以降、製品の安定性と開発の快適性がかなり向上しました。Next.jsはAPIサーバーを作るには多少不便ですが、アーキテクチャをきちんと設計すれば、Next.jsでもかなり使えるAPIサーバーが構築できます。

本記事はNext.jsのバージョン 15 を基準として書いています。ご参考にどうぞ。😉

なぜNext.jsで class‑validator を使うべきか?

Next.jsでAPIリクエストの本文(body)を扱う際、各フィールドが正しい型かどうか、必須項目が抜けていないかを1つ1つチェックするコードを書くのは面倒です。

// 通常のバリデーションロジック
if (!body.email || typeof body.email !== 'string') {
  // エラー処理…
}
if (!body.password || body.password.length < 6) {
  // エラー処理…
}

このような方式だとコードが雑になり、新しいフィールドが追加されるたびにバリデーションロジックを修正する必要が出てきて、煩わしさがあります。

class‑validator を使うと、この問題を解決できます。DTO(Data Transfer Object)クラスにデコレーターを使ってバリデーションルールを宣言的に定義できるので、コードがずっと簡潔かつ直感的に変わります。

class‑validator の設定

ここから本格的にNext.jsプロジェクトに class‑validator を設定していきましょう。

1. 必要なパッケージをインストールする

まず、バリデーションに必要なパッケージをインストールしましょう。

npm install class-validator class-transformer reflect-metadata
  • class-validator:デコレーター基盤のバリデーションライブラリです。
  • class-transformer:純粋な(plain)JavaScriptオブジェクトをクラスインスタンスに変換してくれます。デコレーターがメタデータを正しく読み取るためには、この変換プロセスが必須です。
  • reflect-metadata:デコレーターがランタイムに型情報を読み取れるように手助けするライブラリです。

デコレーター構文を正しく使うために、Babel関連のパッケージもインストールが必要です。

npm install --save-dev @babel/core babel-plugin-transform-typescript-metadata @babel/plugin-proposal-decorators babel-plugin-parameter-decorator

2. tsconfig.json を設定

TypeScriptがデコレーター構文を正しく解釈し、メタデータを生成できるように tsconfig.json ファイルを修正してください。compilerOptions に以下の2つのプロパティを追加します。

// tsconfig.json
{
  "compilerOptions": {// …既存の設定"experimentalDecorators": true,"emitDecoratorMetadata": true
  }
}
  • experimentalDecorators:TypeScriptでデコレーターの使用を許可します。
  • emitDecoratorMetadatareflect-metadata がランタイムに型情報を参照できるよう、メタデータを生成します。

3. .babelrc 設定

プロジェクトルートに .babelrc ファイルを作成し、デコレーター関連のプラグインを追加します。この設定により、BabelがTypeScriptコードを変換する際に、デコレーターのメタデータを維持できるようになります。

// .babelrc
{
  "presets": ["next/babel"],
  "plugins": ["babel-plugin-transform-typescript-metadata",["@babel/plugin-proposal-decorators", { "legacy": true }],"babel-plugin-parameter-decorator",[  "@babel/plugin-transform-runtime",  {    "regenerator": true  }]
  ]
}

4. reflect‑metadata をグローバルに読み込む

reflect-metadata はアプリケーションの開始点で一度だけ読み込むべきです。APIルートのエントリーポイントや共通ユーティリティファイルの冒頭に import "reflect-metadata"; を追加することを推奨します。例えば src/lib/validation.ts のようなファイルを作り、管理すると良いでしょう。

// src/lib/validation.ts または app/api/.../route.ts の先頭
import "reflect-metadata";

// …以降コード

DTO(Data Transfer Object)を定義する

それでは、バリデーションルールを持たせるDTOクラスを作りましょう。src/dto/create-user.dto.ts ファイルを作成し、以下のように書きます。

// src/dto/create-user.dto.ts
import { IsEmail, IsNotEmpty, IsOptional, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsNotEmpty({ message: '名前は必須項目です。' })
  name: string;

  @IsEmail({}, { message: '正しいメール形式ではありません。' })
  email: string;

  @MinLength(6, { message: 'パスワードは少なくとも6文字以上である必要があります。' })
  password: string;

  @IsNotEmpty({ message: '学校情報は必須項目です。' })
  school: string;

  @IsNotEmpty({ message: '電話番号は必須項目です。' })
  phoneNumber: string;

  @IsOptional()
  introduce?: string;
}

各プロパティの上に @IsEmail()@MinLength(6) のようなデコレーターを付与して、簡潔にバリデーションルールを定義できます。デコレーターにカスタムエラーメッセージを追加すれば、検証失敗時により親切なフィードバックが提供できます。

バリデーションユーティリティを作る

NestJSの ValidationPipe のような機能がNext.jsにはないため、DTOを検証するユーティリティ関数を自分で作る必要があります。src/lib/validation.ts ファイルに次の関数を追加してみましょう。

// src/lib/validation.ts
import 'reflect-metadata';
import { validate, ValidationError } from 'class-validator';
import { plainToInstance } from 'class-transformer';

export async function validateDto<T extends object>(
  dtoClass: new () => T,
  payload: unknown,
): Promise<{ instance: T; errors: string[] }> {
  const instance = plainToInstance(dtoClass, payload);
  const errors: ValidationError[] = await validate(instance);

  if (errors.length > 0) {const errorMessages = errors  .map((err) => Object.values(err.constraints || {}))  .flat();return { instance, errors: errorMessages };
  }

  return { instance, errors: [] };
}

この関数は次の2つの重要な役割を果たします。

  1. plainToInstance(dtoClass, payload):純粋なJavaScriptオブジェクト(payload)を、私たちが定義したDTOクラス(dtoClass)のインスタンスに変換します。このプロセスを経ることで、デコレーターに定義されたメタデータが有効になります。
  2. validate(instance):変換後のインスタンスの妥当性を検査し、エラーがあれば ValidationError オブジェクトの配列を返します。

APIルートで使ってみる

それでは、作ったDTOとユーティリティ関数をAPIルートで使ってみましょう。例として app/api/users/route.ts ファイルを見てみます。

// app/api/users/route.ts
import { NextResponse } from 'next/server';
import { CreateUserDto } from '@/dto/create-user.dto';
import { validateDto } from '@/lib/validation';

export async function POST(req: Request) {
  try {const body = await req.json();const { instance, errors } = await validateDto(CreateUserDto, body);
if (errors.length > 0) {  // バリデーション失敗  return NextResponse.json(    { message: 'Validation failed', errors },    { status: 400 },  );}
// バリデーション通過 → DB保存またはビジネスロジック呼び出し// …
return NextResponse.json({  message: '✅ Validation passed',  data: instance,});
  } catch (err: any) {// req.json() のパースエラー等、その他例外処理return NextResponse.json(  { message: 'An unexpected error occurred' },  { status: 500 },);
  }
}

POST リクエストが来たら req.json() で受け取った本文を validateDto 関数に渡します。もし errors 配列に内容があれば、400ステータスコードとともにエラーメッセージを返します。検証を通過したなら、instance オブジェクトを使って次のビジネスロジックを処理すれば良いです。instanceCreateUserDto クラスのインスタンスなので、型が保証されています。

「Controller」を追加する

ルートとコントローラーを分離して、コントローラー側で検証を行うこともできます:

class UserController {
  async createUser(payload: CreateUserDto) {await guardAdmin();await validateDto(CreateUserDto, payload);
return userService.createUserByAdmin(payload);
  }
}

こうすることで関心事をさらに明確に分けることができます。もちろん ServiceRepository も作成できますね。

より良い開発体験のために

Next.jsで class‑validator を使うことは、単にバリデーションコードを減らす以上の意味を持ちます。

  • 可読性および保守性の向上:バリデーションルールがDTOに明確に宣言されているため、コードを理解・修正するのが楽になります。
  • 型安全性の確保plainToInstance を通じて変換されたオブジェクトはクラス型が保証されるため、開発中の型に関する誤りを減らせます。
  • 宣言的プログラミング:「どう」バリデートするかではなく、「何を」バリデートするかに集中でき、より綺麗なコードが書けます。

多少の初期設定は必要ですが、class‑validator が提供する安定性と開発の快適性を考えれば十分に投資する価値があります。皆さんのNext.jsプロジェクトにも導入して、より堅牢で型安全なアプリケーションを作ってみてください。

References