T3 StackはなぜJSONでDate型を扱えるのか

2023/05/06に公開

はじめに

T3 Stackとは、Theo氏によるNext.jsをベースとしたWebアプリのテンプレートです。

動作確認したバージョン

package.json
  "ct3aMetadata": {
    "initVersion": "7.3.2"
  }

疑問

T3 StackでAPIのパラメータにDate型のデータを設定したい時、例えばこのように書けるわけですが、

  • サーバ側
example.ts
  getAll: publicProcedure
    .input(
      z.object({
        date: z.date(),
      })
    )
    .query(({ ctx, input }) => {
      return ctx.prisma.example.findMany({
        where: {
          createdAt: {
            gt: input.date,
          },
        },
      });
    }),
  • クライアント側
test.tsx
  const date = new Date(2023, 5, 6);
  const { data, isSuccess } = api.example.getAll.useQuery({ date });

なぜ、このように書けるのでしょうか。

リクエストのContent-Typeはapplication/jsonなのですが、HTTPでJSONを送るのにクライアント側でJSON.stringifyしたり、サーバ側でJSON.parseしたりしなくていいのでしょうか。また、どうやってその変数がDate型であると伝えるのでしょうか。

mime

答え

答えはtRPCの公式ドキュメントに載っていました。T3 StackではtRPCを使ってサーバ-クライアント間のAPIを実現しているのですが、Data Transformerという機能でDate型を変換してくれているようです。そして変換処理を担っているのはsuperjsonというものだそうです。

https://trpc.io/docs/server/data-transformers

SuperJSON allows us to transparently use, e.g., standard Date/Map/Sets over the wire between the server and client. That is, you can return any of these types from your API-resolver and use them in the client without having to recreate the objects from JSON.

transparentlyとは、おそらく「(Date/Map/Setsなどを送信前にシリアライズしてから、受け取り時にデシリアライズしていることを)意識する必要なく」というような意味でしょう。日本語だとそのまま透過的というようです。

  • サーバ側
src/server/api/trpc.ts
const t = initTRPC.context<typeof createTRPCContext>().create({
  transformer: superjson,
  errorFormatter({ shape }) {
    return shape;
  },
});
  • クライアント側
src/utils/api.ts
export const api = createTRPCNext<AppRouter>({
  config() {
    return {
      /**
       * Transformer used for data de-serialization from the server
       * @see https://trpc.io/docs/data-transformers
       **/
      transformer: superjson,

      /**
       * Links used to determine request flow from client to server
       * @see https://trpc.io/docs/links
       * */
      links: [
        loggerLink({
          enabled: (opts) =>
            process.env.NODE_ENV === "development" ||
            (opts.direction === "down" && opts.result instanceof Error),
        }),
        httpBatchLink({
          url: `${getBaseUrl()}/api/trpc`,
        }),
      ],
    };
  },
  /**
   * Whether tRPC should await queries when server rendering pages
   * @see https://trpc.io/docs/nextjs#ssr-boolean-default-false
   */
  ssr: false,
});

superjsonとは

TypeScriptの標準JSONモジュールの上位互換のようなライブラリのようです。READMEに比較表が載っていますが、Date/Map/Set以外にもundefinedや正規表現、エラーなども扱えるようです。

https://github.com/blitz-js/superjson

実際のリクエストはどうなっているのか

ChromeのDevToolでリクエストのペイロードをのぞいてみると、以下のようにJSON内にDate型であることを表すメタデータを追記しているようです。

superjson

ちなみに

tRPCのtransformerオプションをコメントアウトしてみると、

src/server/api/trpc.ts
const t = initTRPC.context<typeof createTRPCContext>().create({
  // transformer: superjson,
  errorFormatter({ shape }) {
    return shape;
  },
});

フォーマットエラーになりました。確かにsuperjsonが仕事しています。

superjson

まとめ

便利なフレームワークを使っていると色んなことが当たり前のようにできてしまいますが、なぜできているのか?と疑問を向けてみると、その当たり前を支える技術が存在していることに気付かされます。tRPCに感謝。superjsonに感謝。

宣伝

先日、本を書きました!200円で販売中です(無料部分あり)。

https://zenn.dev/ninjin_umigame/books/2619c009c452b6

T3 Stack(Next.js, tRPC, Prisma, NextAuth.js, TailindCSS)を使ったSNSのWebアプリケーション開発について、ユーザーとユースケースの定義から作ったWebアプリのデプロイまで、一通り説明しています。よかったら読んでみてください!

Discussion