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 - 8

Last updated at Posted at 2024-07-07

やりたいこと

  • エラーハンドリングのリファクタリング - エラーハンドリングの処理で共通化できる部分を関数にまとめる
  • 他の例外が発生した場合に使用するエラークラスを用意する
全体像
  1. プロジェクトを作成する
  2. Mysql + prisma でDBを構築する
  3. 環境変数を設定する
  4. APIを作成する
  5. ユーザー登録とログインを実装する
  6. オリジナルのエラーハンドリングを構築する
  7. zod を使ったバリデーションを構築する
  8. エラーハンドリングのリファクタリング
  9. 9
  10. 10
  11. 11
  12. 12

作業対象のファイル

  • src/utils/error-handler.ts
  • src/exceptions/root.ts
  • src/exceptions/bad-request.ts
  • src/exceptions/internal-exception.ts
  • src/exceptions/not-found.ts
  • src/exceptions/unauthorized.ts
  • src/routes/auth.ts
  • src/controllers/auth.ts

エラーハンドリングの際の共通の処理を実装する

/utils/error-handler.ts ファイルを作成する

src/utils/error-handler.ts
import { Request, Response, NextFunction } from 'express'
import { ErrorCode, HttpException } from '../exceptions/root'
import { InternalException } from '../exceptions/internal-exception'

export const errorHnadler = (method: Function) => {
  return async (req: Request, res: Response, next: NextFunction) => {
    try {
      await method(req, res, next)
    } catch (error) {
      let exception: HttpException

      if (error instanceof HttpException) {
        exception = error
      } else {
        exception = new InternalException('サーバーエラーです', ErrorCode.INTERNAL_EXCEPTION, error)
      }

      next(exception)
    }
  }
}

解説

  • ここで定義した errorHnadler 関数は Routes 内で記述されている Controller の関数を引数として受け取る
  • tryブロックでは、引数で受け取ったController の関数を実行する
  • catchブロックでは、エラー検知して処理を行う
    • エラーが HttpException のインスタンスであれば、そのまま HttpException を実行させる
    • そうでない場合、新しい InternalException を実行させる
  • HttpException 以外のエラーが発生するケースの一例は以下
    • 予期せぬTypeScriptランタイムエラー
    • 非同期処理のエラー
    • メモリ不足や他のシステムリソース関連のエラー

サーバーエラー用のErrorクラスを作成する

src/exceptions/bad-request.ts
import { ErrorCode, HttpException } from "./root";

export class BadRequestException extends HttpException {
  constructor(message: string, errorCode: ErrorCode) {
    super(message, errorCode, 400)
  }
}
src/exceptions/internal-exception.ts
import { ErrorCode, HttpException } from "./root";

export class InternalException extends HttpException {
  constructor(message: string, errorCode: ErrorCode, errors: any) {
    super(message, errorCode, 500, errors)
  }
}
src/exceptions/not-found.ts
import { ErrorCode, HttpException } from './root'

export class NotFoundException extends HttpException {
  constructor(message: string, errorCode: ErrorCode) {
    super(message, errorCode, 404)
  }
}
src/exceptions/bad-request.ts
import { ErrorCode, HttpException } from './root'

export class UnauthorizedException extends HttpException {
  constructor(message: string, errorCode: ErrorCode) {
    super(message, errorCode, 404)
  }
}
src/exceptions/root.ts
// 中略 //
export enum ErrorCode {
  USER_NOT_FOUND = 1001,
  USER_ALREADY_EXISTS = 1002,
  INCORRECT_PASSWORD = 1003,
  UNPROCESSABLE_ENTITY = 2001,
  INTERNAL_EXCEPTION = 3001,
  UNAUTHORIZED = 4001,
}

共通化した処理を反映させる

src/routes/auth.ts
import { Router } from 'express'

import { login, me, signup } from '../controllers/auth'
import { errorHnadler } from '../utils/error-handler'  // <-追加

const authRouter: Router = Router()

authRouter.post('/signup', errorHnadler(signup))  // <-追加
authRouter.post('/login', errorHnadler(login))  // <-追加

export default authRouter

解説

  • errorHnadler関数でラップすることで、 signup login が実行される前に errorHnadler で定義した処理を実行することになる

発生したエラーに対して定義したエラークラスを適用させる

src/controllers/auth.ts
export const signup = async (req: Request, res: Response, next: NextFunction) => {
  const result = SignupSchema.safeParse(req.body) // <-変更

  if (!result.success) {
    throw new UnprocessableEntityException('入力された値に誤りがあります', ErrorCode.UNPROCESSABLE_ENTITY, result.error.errors)
    } // <-変更※next ではなく throw にしている

  const { email, password, name } = result.data

  const existingUser = await prisma.user.findFirst({
    where: {
      email,
    },
  })
  
  if (existingUser) {
    throw new BadRequestException('既に存在するユーザーです', ErrorCode.USER_ALREADY_EXISTS)
  } // <-変更※next ではなく throw にしている
  
  const user = await prisma.user.create({
    data: {
      email,
      name,
      password: hashSync(password, 10),
    },
  })

  res.json({ user }) 
}

export const login = async (req: Request, res: Response) => {
  const result = SignupSchema.safeParse(req.body) // <-変更

  if (!result.success) {
    throw new UnprocessableEntityException('入力された値に誤りがあります', ErrorCode.UNPROCESSABLE_ENTITY, result.error.errors)
  } // <- 追加

  const user = await prisma.user.findFirst({
    where: {
      email,
    },
  })

  if (!user) {
    throw new NotFoundException('ユーザーが見つかりませんでした', ErrorCode.USER_NOT_FOUND)
  }  // <-変更※next ではなく throw にしている

  const validPassword = compareSync(password, user.password)

  if (!validPassword) {
    throw new BadRequestException('パスワードが間違っています', ErrorCode.INCORRECT_PASSWORD)
  } // <-変更※next ではなく throw にしている

  const token = jwt.sign({ id: user.id }, JWT_SECRET, { expiresIn: '1h' })

  res.json({ user, token })
}

解説

zodの safeParse メソッドについて

  • リクエストボディのバリデーションを行い、もしreq.body の内容が SignupSchema の定義と一致しない場合は、result の中身が success: false となる
  • バリデーションが通っている場合は、result の中身が success: true となり、result.data にバリデーションを通過したデータが格納される

zodの parse メソッドについて(変更前で記述していた方法)

  • リクエストボディのバリデーションを行い、もしreq.body の内容が Schema の定義と一致しない場合 ZodError が投げられまる

try-catch で囲わなくなった理由

  • src/routes/auth.ts ファイル内で errorHnadler(signup) errorHnadler(login) と記述している通り、 errorHnadler の処理を挟んでいるから
  • errorHnadler のコード内で try-catch をしていて、ここでエラーハンドリングをしているため、Controllerの処理の中で try-catch で囲う必要がなくなったから

next関数 ではなく throw関数 に変更した理由

  • errorHnadler の実装により、ハンドラー関数内で発生した例外は自動的にキャッチされ、適切に処理されるようになったから
    ※ 従来の実装はこちら
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?