Masayan tech blog.

  1. ブログ記事一覧>
  2. ExpressとPrismaでサクッと作るAPI:Part4(APIのエラーハンドリング)

ExpressとPrismaでサクッと作るAPI:Part4(APIのエラーハンドリング)

公開日

目次

  • 環境
  • APIのエラーハンドリング実装

環境

フロントエンドアプリケーション

  • Next v13.4.19
  • react v18.2.0
  • TypeScript v5.2.2

API

  • Node v18.17.1
  • Express v4.18.2
  • TypeScript v5.2.2

DB

  • Prisma v5.2.0
  • mysql v8.0

ローカル環境

DockerAPIのCRUD実装

Prisma固有のエラー

公式ドキュメントを見ると、Prisma固有のエラーは以下のように大別できる。

  • PrismaClientKnownRequestError
  • PrismaClientValidationError
  • PrismaClientRustPanicError
  • PrismaClientInitializationError
  • PrismaClientUnknownRequestError

さらにこのうち、PrismaClientKnownRequestErrorはcodeというプロパティを持っており、このコードによりかなり細分化されている(例としては、メールアドレスのユニーク制約違反など)

PrismaClientKnownRequestError

Prismaクライアントにより、Code別であらかじめ分類されている既知のエラー

PrismaClientValidationError

新規レコードを作成する際に必須のキーが存在していない場合に生じるエラー

PrismaClientRustPanicError

基盤となるエンジンがクラッシュし、ゼロ以外の終了コードで終了した場合に生じるエラー

PrismaClientInitializationError

データベースへの接続に失敗するなどの場合に生じるエラー

PrismaClientUnknownRequestError

上記に分類されないエラー

APIのエラーハンドリング実装

CRUDのうち、新規作成でエラーハンドリングを実装する。新規作成以外も基本的に同じように実装できるか、これよりも簡単なはずなので本記事では時間の関係上割愛する。

方針

名前、メールアドレスいずれも必須としており、かつメールアドレスにはユニーク制約がある。そのため、新規作成時にエラーが生じた場合は

  • PrismaClientKnownRequestErrorまたはPrismaClientValidationErrorであれば、対応するエラーメッセージをステータスコード4XX系で返却
  • 上記以外であれば、システムエラーである旨のエラーメッセージをステータスコード500で返却

とする。(※プロダクトコードであれば、もう少し深く設計/検討した方がいいが、チュートリアルなのでこれくらいでいいと思う)

実装の全体像

あくまでサンプルだが以下のようになった

import express, { Request, Response } from "express";
import { PrismaClient, User } from "@prisma/client";
import { CreateUserUseCase } from "../useCase/CreateUserUseCase";
import {
  PrismaClientKnownRequestError,
  PrismaClientValidationError,
} from "@prisma/client/runtime/library";

const router = express.Router();

/**
 * ユーザー新規作成
 */
router.post("", async (req: Request, res: Response) => {
  const { name, email } = parseReqBody(req);

  try {
    const useCase = new CreateUserUseCase();
    const user = await useCase.create(name, email);

    return res.status(200).json({
      user,
    });
  } catch (e: unknown) {
    console.log(e);
    // System error & PrismaClientRustPanicError, PrismaClientInitializationError, PrismaClientUnknownRequestErrorで使用する
    let statusCode = 500;
    let errorMessages = ["System error"];

    /**
     * 予想されるエラー
     *
     * - 1.PrismaClientKnownRequestError
     *   - instanceofとerror.codeを分岐して対応する
     * - 2.PrismaClientValidationError
     *   - instanceofで対応
     * - 上記以外は共通のシステムエラーで対応(500)
     */
    if (e instanceof PrismaClientKnownRequestError) {
      if (e.code === "P2002") {
        statusCode = 400;
        errorMessages = ["The email address is already in use"];
      }
    }

    if (e instanceof PrismaClientValidationError) {
      const regex = /Argument.*?missing./g;
      const matches = e.message.match(regex);

      statusCode = 400;
      if (matches) {
        errorMessages = matches;
      }
    }

    return res.status(statusCode).json({
      statusCode,
      error: errorMessages,
    });
  }
});

ポイント

上記の通り、共通のサーバー側でのシステムエラー、データベースに接続できないなど(PrismaClientInitializationErrorなど)はブラウザには500でエラーメッセージを返却し、(実際は)管理者へのエラーの通知などを行うため、最初に変数として用意しておく

catch (e: unknown) {
    // System error & PrismaClientRustPanicError, PrismaClientInitializationError, PrismaClientUnknownRequestError
    let statusCode = 500;
    let errorMessages = ["System error"];

次に、Prisma既知のエラーでcodeプロパティを持っている場合(PrismaClientKnownRequestError)であれば、instanceofで判定し、続いてe.codeと一致するかで分岐。今回はユニーク制約のコードがP2002なのでこれで判定する。

if (e instanceof PrismaClientKnownRequestError) {
    if (e.code === "P2002") {
        statusCode = 400;
        errorMessages = ["The email address is already in use"];
    }
}

続いてnameとemailがきちんとブラウザから送信されているかをチェックする。

if (e instanceof PrismaClientValidationError) {
    const regex = /Argument.*?missing./g;
    const matches = e.message.match(regex);

    statusCode = 400;
      
    if (matches) {
        errorMessages = matches;
    }
 }

上記で正規表現を使っているのは、以下のようなエラーメッセージから、どのパラメーターが不足しているのかというのを取り出すため。(ここは少し使いずらさがある・・・・)ステータスコード400で取り出したエラーメッセージを返却する

Invalid `prisma.user.create()` invocation:

{
  data: {
    name: "",
+   email: String
  }
}

Argument `email` is missing.

これ以外のエラーについては、ステータスコード500で共通のシステムエラーメッセージを表示する。

ちなみに、500エラーを試したければ、DBコンテナを停止してAPIにリクエストを送信する(PrismaClientInitializationError)か、tryブロックでthow new ErrorとしてあげればOK。

return res.status(statusCode).json({
   statusCode,
   error: errorMessages,
});

まとめ

いかがでしたでしょうか。本記事は、ExpressとPrismaでAPIをサクッと作るシリーズのPart4です。Part4では主にPrismaでのエラーの分類やAPIのエラーハンドリングを実装しました。Part5では主にPrismaでのリレーションや高度なカラム設定を行います