0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeExpressTutorial - 7

Last updated at Posted at 2024-07-06

やりたいこと

  • zodを使ってバリデーションを定義したい
  • バリデーションが引っ掛かった場合にエラーメッセージのJSONを返したい
全体像
  1. プロジェクト作成
  2. DB構築 - Mysql + prisma
  3. 環境変数設定
  4. API設定 - 簡略版
  5. ユーザー登録とログイン
  6. カスタムエラーハンドリング
  7. バリデーション - zod
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12

zodとは

TypeScriptのためのスキーマ宣言・バリデーションライブラリです
バリデーションを定義し、実行時にその形状を検証できます
複雑なデータ構造の検証、エラーメッセージのカスタマイズ、型推論との統合が可能です
zod公式

作業対象のファイル

  • src/schema/users.ts
  • src/controllers/auth.ts
  • src/exceptions/root.ts
  • src/exceptions/validation.ts

バリデーションの条件を設定する

/schema/users.ts ファイルを作成する(ここにバリデーションの条件を記述する)

src/schema/users.ts
import { z } from 'zod'

export const SignupSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  password: z.string().min(6)
})

エラーコードのEnumを追加する

src/exceptions/root.ts
export enum ErrorCode {
  // 中略 //
    
  USER_NOT_FOUND = 1001,
  USER_ALREADY_EXISTS = 1002,
  INCORRECT_PASSWORD = 1003,
  UNPROCESSABLE_ENTITY = 2001,
  INTERNAL_EXCEPTION = 3001,
}

バリデーション用のErrorクラスを作成する

/exceptions/validation.ts ファイルを作成する

src/exceptions/validation.ts
import { ErrorCode, HttpException } from "./root";

export class UnprocessableEntityException extends HttpException {
  constructor(message: string, errorCode: ErrorCode, errors: any) {
    super(message, errorCode, 422, errors);
  }
}

解説

  • HttpException クラスを拡張(継承)して UnprocessableEntityException クラスを定義している
  • 継承することで、HttpException の基本的な機能を継承しつつ、特定のユースケース(今回のケースでは HTTP 422エラー)に特化したエラークラスを作成している

constructor では何をやっているか

  • クラスのインスタンスが作成されるときに呼び出される特別なメソッド
  • UnprocessableEntityException クラスのインスタンスが作られた時に、指定した引数を受け取る必要がある

super() では何をやっているか

super() を使うことで、親クラス(HttpExceptionクラス)のインスタンスを作る時に必要な引数を指定している

Controller内でエラーが発生する箇所に反映させる

src/controllers/auth.ts
export const signup = async (req: Request, res: Response, next: NextFunction) => {
  try { // try-catchを追加
    SignupSchema.parse(req.body)   // <-追加

    const { email, password, name } = req.body

    const existingUser = await prisma.user.findFirst({
      where: {
        email,
      },
    })
  
    if (existingUser) {
      return next(new BadRequestException('既に存在するユーザーです', ErrorCode.USER_ALREADY_EXISTS))
    }
  
    const user = await prisma.user.create({
      data: {
        email,
        name,
        password: hashSync(password, 10),
      },
    })
  
    res.json({ user })
  } catch (error: unknown) { // <-追加
    if (error instanceof ZodError) { // <-追加
      next(new UnprocessableEntityException('ユーザーの作成に失敗しました', ErrorCode.UNPROCESSABLE_ENTITY, error.issues)) // <-追加
    }
  }
}

解説

parse メソッドについて

  • req.body (リクエストのボディデータ)を Schema で定義されたルールに基づいて検証して、もしバリデーションを書けて真偽を判定するメソッド
  • 今回のケースでは、 req.body の値が SignupSchema で定義したバリデーションの条件に引っ掛かったら、その時点で例外をスローされる -> catch ブロックに処理が進む

catchブロックの処理 - なんで any型じゃなくて unknown型 を使っているか

  • unknown 型を使用することで、開発者は明示的に型をチェックしたり、型アサーションを行ったりする必要があるため、エラーオブジェクトの構造や型を適切に検証してから使用することが強制されて堅実な設計ができるため
  • エラーオブジェクトの構造を確認するために instanceof を使ってチェックしている
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?