Open8

tRPCのメモ

shmurakamishmurakami

tRPCを使ってみたメモ。
とりあえずチュートリアル通りに動かしてみるのと、認証、非モノレポ構成、Next.jsから叩いてみるあたりまでやってみたい。

shmurakamishmurakami

サーバー側のコードはとりあえず雑にQueryのみ。Mutationは後で。

router.ts
export const r = router({
  taskList: publicProcedure.query(getTaskList),
});
procedure/query.task.ts
export const getTaskList = async (): Promise<Task[]> => {
  return [
    { id: "1", task: "Buy milk", status: "todo" },
    { id: "2", task: "Exercise", status: "done" },
    { id: "3", task: "Study", status: "todo" },
  ];
};

固定値を返す形にしてテストを書いてみる。

shmurakamishmurakami
task.test.ts
describe("taskList procedure", () => {
  it("should return the list of tasks", async () => {
    const result = await r.taskList({
      path: "",
      rawInput: undefined,
      type: "query",
      ctx: {},
    });
    const expected = await getTaskList();

    expect(result).toEqual(expected);
  });
});

こんな形でprocedureの呼び出しをしてテストを実行できる。
procedureの呼び出しの引数が多くて記述がめんどくさいのどうにかならないかな。

shmurakamishmurakami
task.test.ts
describe("taskList procedure", () => {
  const ctx = {};
  const createCaller = createCallerFactory()(r);
  const caller = createCaller(ctx);

  it("should return the list of tasks", async () => {
    const result = await caller.taskList();
    const expected = await getTaskList();

    expect(result).toEqual(expected);
  });
});

v10のテストはこう書くのが良さそう。

shmurakamishmurakami

mutationとqueryのコードをどこに置くか迷ってそれぞれ別のディレクトリに置いたけどあまり良いアイデアではなさそう。

routes.ts
export const r = router({
  taskList: publicProcedure.query(getTaskList),
  addTask: publicProcedure.input(taskSchema).mutation(addTask),
});

routeがこんな書き方になり見づらいし、特にinputに対してスキーマを定義しようと思うとどう書くかが悩ましい。スキーマと実態をそれぞれexportする、もしくはpublicProcedureを渡してセットするような感じになるが、どちらも良くない。
ドキュメント通りに

publicProcedure.input({...schema}).mutation(async (opts) => {...})

と書いた方が視認性が良さそう。
公式のexamplesに置かれてるようにドメインごとにquery, mutationを管理して

import { task } from "./procedure/task";
export const r = router({
  task: taskRouter,
});
task.ts
export const taskRouter = router({
  list: publicProcedure.query(list),
  add: publicProcedure
    .input(
      z.object({
        task: z.string().min(1),
      }),
    )
    .mutation(add),
});

こんな形にした方が視認性が良さそう。

shmurakamishmurakami

モノレポではないtRPCのアプリケーションをスキーマファーストで開発することを考える。
tRPCは基本的にはモノレポ向けだと思われるが、スタートアップだったり小さなチームでも無く、プロダクトだったりチームがスケールすることを考えるとモノレポでフロントエンドもバックエンドも同じリポジトリに置いとくのが得策とは思えないんだよな。

trpcのスキーマを公開するためのツールとしてOpenAPIとts-restを比較した記事があった。
https://catalins.tech/public-api-trpc/

trpc-openapi
https://github.com/jlalmes/trpc-openapi
ts-rest
https://ts-rest.com/

非モノレポをしようと思うとOpenAPIになるんだろうかと思っていたけど、ts-restっていうものを初めて知った。
TS-first で開発していくスタイルらしい。tRPCな時点でフロントエンドもバックエンドもTSにはなるから言語の都合は気にならなそう。名前に rest と入ってるけど、特段RESTFulであることを強制するわけでもない、のかな。
contract つまり契約、スキーマとして扱えるものを先所に書いて、それに沿ってバックエンド、フロントエンドを書く。
結局そのcontractをどうやって両端のリポジトリで参照するのかというのがあるけど、 NX を推奨してるらしい。
フロントエンドとバックエンドの中間層になるものらしいけど、これもよく分からないからそのうち調べたい。
https://ts-rest.com/docs/guides/nx

雰囲気は分かったのでOpenAPIを試してみる。
基本的にはドキュメント通りに進めていき

const t = initTRPC.meta<OpenApiMeta>().create();

trpcのinitのときにmetaとしてOpenApiのメタ情報を指定したり、serverのhandlerがOpenApiHandlerになる。

const server = http.createServer(
  createOpenApiHttpHandler({
    router: r,
    createContext: () => {},
    responseMeta: () => {},
    onError: () => {},
    maxBodySize: 1024 * 1024 * 100,
  }),
);

このときのオプションが全部指定しないとビルドが通らなかったけど、router以外は無くても良いっぽいのでよく分からない。
generateOpenApiDocument を実行すると

{
  openapi: '3.0.3',
  info: {
    title: 'tRPC OpenAPI',
    description: 'tRPC API Schema',
    version: '0.0.1'
  },
  servers: [ { url: 'http://localhost:3333' } ],
  paths: {
    '/task/list': {
      get: {...}
    },
    '/task/add': {
      post: {...}
    }
  },
  components: {
    securitySchemes: { Authorization: { type: 'http', scheme: 'bearer' } },
    responses: {
      error: { description: 'Error response', ...}}},
  tags: undefined,
  externalDocs: undefined
}

だいぶ省略したけどこういうのが出力できる。あー、OpenAPIってこんな感じの定義だった気がするなー、という感じ。
昔Swagger Editorが言うこときかなすぎてストレスが溜まった記憶だったりOpenAPIのドキュメントの運用ができなくて破綻した記憶が蘇ってきた。

コード的には各procedureのinput, outputが必須になりそうだったり(プロダクション運用するなら必須なんだろうけど)、procedureごとにmeta情報を結構書かなきゃいけないのがどうにもめんどくさく感じる。
単純にOpenAPIが好きじゃないだけな気がしなくもない。

shmurakamishmurakami

どうもtrpc-openapiはclient側には対応してないみたいだな?サーバーからドキュメント生成はできるから、あとはOpenAPIと好きなHTTPクライアントでやってくれ、という感じなのかなー。

shmurakamishmurakami

trpcに関して見たいものはだいたい見れたかなと思うので一旦ここまで。クライアントサイドでどうHTTPリクエストに変換してるかとかはSubscription周りもちょっと気になったけど。

やはり基本的にはモノレポでクライアントとサーバーが同居している状態で使うのが望ましいのだと思う。
けどプロダクトやチームが育って関係者が増えたら維持しづらくない?というのがどうしても気になる。小さいチームだったり個人開発で使う分には良さそうに思う。

現時点で11 betaがリリースされている状態で、もう少しでメジャーバージョンが11に上がるという状態。
一度セットアップでミスってサーバーは10だけどクライアントが11という状態になったときにビルドが通らなかったので、メジャーバージョン間での互換性は基本的に無いらしい。
モノレポだろうと、メジャーバージョンを上げたい場合は安全に上げられるんだろうか。サーバーとクライアントを同時に上げないといけないのでは。
同時にリリースしたとして、ユーザーサイドで更新するまでサーバーは11でクライアントは10という状態になりうるけど、その状態で正しく動くのかは不安が残る。