🧊

ts-restをCloudflare Workersで動かしたかった

が、断念した。また今度やるかもしれないし、やらないかもしれないので、とりあえずその経緯を書き残しておく。

あらすじ

  • APIを実装すると、型付きクライアントが生成される系のソリューションを探してた
  • そしてCloudflare Workersでも動いてほしい

まず最初に検討したのがtRPCで、既にCloudflare Workers向けの実装があった。

Cloudflare WorkersでもtRPCを使う | Memory ice cubes https://leaysgur.github.io/posts/2023/08/29/170459/

ただし、現状のtRPCはJSONでしかやり取りする想定がないらしく、バイナリをやり取りできない。

Support for additional Content-Types · Issue #1937 · trpc/trpc https://github.com/trpc/trpc/issues/1937

今の時点では、署名付きURLを発行するなり、tRPCとは別ルートでアップロードする・・とかしかない。

が、tRPCのクライアントとバイナリ用のRESTクライアントの併用なんかやりたくない。

そこで、同様のことができる別のソリューションを探していて、ts-restを見つけた。

ts-rest

ts-rest/ts-rest: RPC-like client, contract, and server implementation for a pure REST API https://github.com/ts-rest/ts-rest

これはどんなものかというと、

  • tRPCと同様に、単一の定義からクライアントとサーバーの実装を作る方式
    • ts-rest語でContractと呼ぶ
  • ただ厳密には、tRPCではその定義が実装そのものである一方、ts-restではただの構造体
    • 逆に言うと、実装は別に用意しないといけない
  • RESTライクに好きなようにURLを決められる
  • OpenAPIの定義も出力できる

という感じ。

今回の経緯でいくと、最大の差別化要因は、multipart/form-dataに対応してる点。

バリデーションがまたしてもzod一択ってところが個人的にはネック(ランタイムでの使用はマストではないが、内部的な型がどっぷり依存してるとのこと)だが、それはひとまず置いておくとして。

Cloudflare Workersで動かない

問題はコレ。動かないというか、それ用のサーバー側実装がまだない。

  • NestJS
  • Next.js
  • Fastify
  • Express

現時点で用意されてるのはこの4つ。

ないなら作ればいい

最初はそう思ってた。

既存のサーバー側実装を見ると、Contractを唯一のソースとする以外は、フォーマットがないというか、それぞれフレームワーク向けによしなにしろって感じだった。

ちなみに、ContractはこういうTSファイル。これはzodを使ってないバージョン。

import { initContract } from "@ts-rest/core";

const c = initContract();

export const contract = c.router({
  createPost: {
    method: "POST",
    path: "/posts",
    responses: {
      201: c.type<Post>(),
    },
    body: c.type<{title: string}>(),
    summary: "Create a post",
  },
  getPost: {
    method: "GET",
    path: "/posts/:id",
    responses: {
      200: c.type<Post | null>(),
    },
    summary: "Get a post by id",
  },
});

目をこらすと共通の型みたいなのはあるけど、Fastifyはプラグインとして実装してるがNestJSはデコレーターだったりと、大枠の実装はまちまち。

逆に言えば材料はこれしかないので、

  • このオブジェクトの各エントリーをループで回し
  • router.get("/posts", fn)みたいな各ルーターのAPIを呼んで登録し
  • 任意の場所にあるであろうハンドラの実装に対して、検証したInputを渡し
  • ハンドラから帰ってきたOutputを検証してレスポンス

というようなコードを用意すればいい。

が、個人的に、もう特定のフレームワークやルーターにどっぷりなものをあまり書きたくないという思いがあり、ここで手を止めた。

ただ特定のフレームワークに依存しないようにしようとすると、どうしてもコードが冗長になってしまう(たとえば、HTTPメソッドとパスの定義が重複する)し、DXもイマイチであまり旨味がないなーとも。

型付きクライアントを出力するのが目的であれば、URLも別にREST風である必要ないよなってことにも気付き、モチベーションが枯れたところ。

ニッチなところを攻めてるな、とは思う。