TypeExpressTutorial - 8

:::note info
やりたいこと
- エラーハンドリングのリファクタリング - エラーハンドリングの処理で共通化できる部分を関数にまとめる
- 他の例外が発生した場合に使用するエラークラスを用意する
:::

<details><summary>全体像</summary>

1. [プロジェクトを作成する](https://qiita.com/yk-graph/items/b5f0551cb368fb86c50c)
1. [Mysql + prisma でDBを構築する](https://qiita.com/yk-graph/items/02333bd99b625dbcc49d)
1. [環境変数を設定する](https://qiita.com/yk-graph/items/e37cd011c6da3feb93d2)
1. [APIを作成する](https://qiita.com/yk-graph/items/e5b52a4d35cd43d95f7a)
1. [ユーザー登録とログインを実装する](https://qiita.com/yk-graph/items/dd093a286377843441e1)
1. [オリジナルのエラーハンドリングを構築する](https://qiita.com/yk-graph/items/fb47753e55986bafdb1d)
1. [zod を使ったバリデーションを構築する](https://qiita.com/yk-graph/items/84214e03a6119523ac18)
1. [エラーハンドリングのリファクタリング](https://qiita.com/yk-graph/items/8951b710158eb1a20eb7)
1. [9]()
1. [10]()
1. [11]()
1. [12]()
</details>

## 作業対象のファイル
- 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` の実装により、ハンドラー関数内で発生した例外は自動的にキャッチされ、適切に処理されるようになったから
※ 従来の実装は[こちら](https://qiita.com/yk-graph/items/fb47753e55986bafdb1d#%E3%82%A8%E3%83%A9%E3%83%BC%E5%87%A6%E7%90%86%E3%82%92%E8%A1%8C%E3%81%86%E9%9A%9B%E3%82%A8%E3%83%A9%E3%83%BC%E3%82%92throw%E3%81%99%E3%82%8B%E3%81%AE%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%8Fnext%E9%96%A2%E6%95%B0%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B%E7%90%86%E7%94%B1)