ymmr
Recent Articles
    公開日: 2023/10/17

    [Next.js] API Route のハンドラーからレスポンスのデータ型を取り出す

    Next.js で REST API をつくっていたところ、レスポンスのデータ型を何度も繰り返し書いていることに気づきました。コードを DRY にするために何か良い方法はないかと考えたので、記事としてまとめます。

    問題のコード

    レスポンスのデータ型は至るところで活躍します。例えば、MSW を用いたモックの定義や、SWR によるデータ取得のコードで散見されました。

    /pages/api/members.ts
    import { getAllMembers } from '@/lib/members';
    
    import type { Member } from '@prisma/client';
    import type { NextApiRequest, NextApiResponse } from 'next';
    
    const handler = async (
      request: NextApiRequest,
      response: NextApiResponse<{ members: Member[] }>,
      //                               ^^^^^^ 共有したいレスポンスのデータ型
    ) => {
      if (request.method === 'GET') {
        const members = await getAllMembers();
    
        response.json({ members });
      }
    };
    
    export default handler;
    
    /mocks/resolvers/members.ts
    const get: ResponseResolver<
      RestRequest<never, PathParams<string>>,
      RestContext
    > = (_, response, context) => {
      return response(
        context.status(200),
        context.json<{ members: Member[] }>({ members }),
      //                   ^^^^^^ モック用に使ったり...
      );
    };
    
    /components/Members/index.tsx
    const Members = () => {
      const { data, isLoading } = useSWR<{ members: Member[] }>(
        //                                     ^^^^^^ データ取得に使ったりする
        '/api/members',
        (url: string) =>
          axios.get<{ members: Member[] }>(url).then((response) => response.data),
          //               ^^^^^^ ここでも...
      );
    
      // return <>...</>
    

    繰り返しを排除する

    API の型定義からデータ型を取り出せばよいと考え、次のようなユーティリティを作成しました。

    /types/utils.ts
    import type { NextApiRequest, NextApiResponse } from 'next';
    
    export type ResponseData<T> = T extends (
      request: NextApiRequest,
      response: NextApiResponse<infer U>,
    ) => void | Promise<void>
      ? U
      : never;
    

    Generic で API の型を受け取り、Conditional Type と infer の組み合わせでレスポンスのデータ型を取り出しています。呼び出し側のコードは以下のように書き換えられます。

    /mocks/resolvers/members.ts
    diff --git a/src/mocks/resolvers/members.ts b/src/mocks/resolvers/members.ts
    index 84c4029..9723d99 100644
    --- a/src/mocks/resolvers/members.ts
    +++ b/src/mocks/resolvers/members.ts
    @@ -1,6 +1,7 @@
     import { members } from '../fakeData/members';
    
    -import type { Member } from '@prisma/client';
    +import type handler from '@/pages/api/members';
    +import type { ResponseData } from '@/types/utils';
     import type {
       PathParams,
       ResponseResolver,
    @@ -14,7 +15,7 @@ const get: ResponseResolver<
     > = (_, response, context) => {
       return response(
         context.status(200),
    -    context.json<{ members: Member[] }>({ members }),
    +    context.json<ResponseData<typeof handler>>({ members }),
       );
     };
    $
    

    これでデータ型が必要なコードにおいて、同じ型定義を毎回書く必要はなくなりました。また、API エンドポイントが増えた場合も同じようにユーティリティを再利用できます。

    おわりに

    記事を書いてから考え直したのですが、このユーティリティは果たして使いやすいのでしょうか・・・。公式ドキュメントのサンプルコードにあるとおり、愚直に API の近くでレスポンスのデータ型を定義した方がよいのかもしれません。今は結論を出せないので、よいプラクティスがあれば取り入れていきたいです。