🌪️

Nextjs(App router)でのRestAPIのエラーハンドリングについて

2024/07/28に公開

記事の動機: NextjsはAPIの実装周りについてあまりサポートが強くない?

今年に入ってから、Nextjsを利用したWebアプリのプロジェクトに携わっています。
プロジェクトの特性上、バックエンドの実装(DBアクセスやRESTベースでの他システム呼び出しなど)の実装が必要な一方でバックエンドのためにプロジェクトを分けて...とやっていくのが面倒なため、Nextjsを採用しました。
使っていくと感じましたが、NextjsってあまりAPIの実装関してフレームワークとしてのサポートが強くないと思います。
少なくとも公式サイトや(ちょっと古めの)Awesomeリストではあまりサポートが強くなさそうです。
https://nextjs.org/docs/app/building-your-application/routing/route-handlers

https://github.com/unicodeveloper/awesome-nextjs
そこで自分でうまいことエラーを処理することにしました。

問題点

自分の調査不足かもしれませんが、何も考えずにNextjsのエラーハンドルする場合は、次のような書き方が一般的にはありうると思います。
下記の例は、各APIごとにいちいちTry-catchを書いていますが、これをやっていくとサポートするExceptionが増えれば増えるほどメンテナンス箇所が増える面倒さもあるため辞めたい例です。

api/users/route.ts
export const GET = async (request: NextRequest) => {
  try {
    // Get paramaters from URL
    const searchParams = request.nextUrl.searchParams;
    const arg = searchParams.get("some_param");

    const users = await ListUsers(arg);

    return NextResponse.json({ users: users });
  } catch (e) {
    // Point: 各APIでこのcatch句の実装をしないといけない...
    if (e instanceof NotFoundException) {
      return NextResponse.json({ message: e.message }, { status: 404 });
    }

    if (e instanceof BadRequestException) {
      return NextResponse.json({ message: e.message }, { status: 400 });
    }

    return NextResponse.json({ message: e.message });
  }
};

対策

https://nextjs.org/docs/app/api-reference/file-conventions/route
Nextjsのroute.jsのDocsを見ると、サポートしているHTTPメソッド名に対する型の記載があります。ここと一致する形で既存のAPIをWrapする関数を実装します。
具体的には、次のようにroute.ts側で呼び出せるように実装します。

api/users/route.ts
// この関数ではTry-catchを実装しない。
// ビジネスロジックに関わる内容に専念する
const getMethod = async (request: NextRequest, context?: unknown) => {
    const searchParams = request.nextUrl.searchParams;
    const arg = searchParams.get("some_param");

    const users = await ListUsers(arg);

    // Point: 通常のResponseだけ実装すればよい
    return NextResponse.json({ users: users });
}


// Wrapping each method
// ここではGETの引数と返り値を満たす形定義したWrapperをつくって呼び出す。
export const GET = APIHandler(getMethod);

route.tsの使用上、各メソッドに対応する関数は 各メソッド: (request: NextRequest, context?: any) => Promise<NextResponse>
型を返す関数を実装する必要があります。
そこで、APIHandlerは下記のように関数をまるまる受け取って、中でTry-catchするようにします。

handler.tsx
export const APIHandler = (
  restHandler: (request: NextRequest, context?: any) => Promise<NextResponse>
) => {
  return async (request: NextRequest, context?: any) => {
    try {
      // Act rest method (Ex. get, post, put...)
      return await restHandler(request, context);
    } catch (e) {
      console.error(`Error... ${JSON.stringify(e)}`);

      // Use your defined exceptions
      if (e instanceof CustomException) {
        return NextResponse.json<ExceptionResponse>({
          message: `Error occured... message: ${e.message} cause: ${e.cause}`,
        });
      }

      // fallback
      return NextResponse.json<ExceptionResponse>({
          message: `Unexpected error occured.`,
        });
    }
  };
};

このようにすることで、APIHandlerの中身の整理やAPIHandlerを補助する関数などの実装をすることで、効率の良い変更ができる他、自動テストもシンプルになると考えられます。

補足

なお、この考え方をReact側(NextのFrontend側)に適用すると、HOC(高階コンポーネント)になるようですが、現在はあまり推奨されていないようでした。
確かにhooksの仕組みでなんとかなりますし、ビジネスロジックの隠蔽がシンプルになりますね...
認可の管理など、一部の領域では使えそうな考え方だと思うので、HOC自体は覚えておいても良いかなと思います。

https://ja.legacy.reactjs.org/docs/higher-order-components.html

Discussion