Kori (氷) というTypeScript用のウェブアプリケーションフレームワークをCursorたちと一緒に作ってみている

とりあえずコンセプトは動きそうだなぁってくらいで、ちゃんと動くことも確認してないし、テストも書いてないし、まだまだやることはたくさんあるんだけど、どっかでいったんブログに書いて休憩しようと思ったので、書くことにした。年内である程度動くところまで持っていけたらいいな。

Kori

  • Kori (英語版を作ってその翻訳をCursorにお願いしたのでそういう感じの日本語になってます)

特徴

TypeScriptの型安全さをわりといっぱい活かしてコードを書ける。

スキーマを定義すると、そのスキーマにしたがってバリデーションが実行されて、その結果を型安全に扱える。その同じスキーマをOpenAPIのスキーマとしても利用できる。スキーマの実装としては、とりあえずZod v4に対応しておいた。

たとえばこんな感じで定義すると

const UserSchema = z.object({
  name: z.string().min(1).max(100).meta({ description: 'User name' }),
  age: z.number().int().min(0).meta({ description: 'User age' }),
});

app.post('/users', {
  pluginMetadata: openApiMeta({
    summary: 'Create user',
    description: 'Create a new user with validation',
  }),
  requestSchema: zodRequestSchema({
    body: UserSchema,
  }),
  handler: (ctx) => {
    const { name, age } = ctx.req.validatedBody();

    const newUser = createUser(name, age);

    return ctx.res.status(HttpStatus.CREATED).json(newUser);
  },
});

ScalarというUIでOpenAPIのドキュメントを確認できる。

Scalarは知らなかったんだけど「Swagger UI以外にもなんかある?」ってCursorに聞いたら教えてくれた。

KoriのエンジンはHonoのルーター

Koriのエンジンには、Honoのルーターを使っている。ウェブアプリケーションフレームワークとしていちばん大切な部分を、Honoのルーターに任せているので安心。

そこからFetch APIを生成している。まだNode.jsでしか動かしたことがないけど、たぶんDenoやBunでも動くんじゃないかなぁと思っている。Node.jsもHonoのNode.jsアダプターをラッピングして動かしている(パフォーマンスチューニングしてあってすごい)。

はじまり

Hono好きだなぁって思って触っていて、OpenAPIのドキュメント生成まわりを見ていたら「ちょっといじって、こんな感じで実装とOpenAPIがつながってたら自分の好みだなぁ」って気持ちになって、最初はHonoのプラグインを作ろうとしてたんだけど、Honoの作り的に完全に自分好みにするのはなかなか難しいかーって思ってそこで終了していた。

その数カ月後に、CursorやClaude Codeを触り始めて、生成AIってちょこっと使ってみる分には良さそうだけど、ちゃんとしたものを作ろうとしたらどうなるんだろう?仕事よりももっと振り切った感じで使ってみたいな、あ、そうだそういえばHonoのOpenAPIプラグイン作ってみたいんだった・・・フレームワーク自体を作ってみるか・・・みたいなノリ。

ということで、Honoのルーターをラッピングして、自分好みの型安全さでコードを書けるフレームワークになっている(しようとしている)。

名前

深く考えずに「氷」にした。KoriでHonoをラッピングしてたら溶けるよな・・・と思いながら。

生成AIとの協業

Cursorで書き始めて、途中でClaude CodeやDevinを使ったりしながら、やっぱりCursorに戻ってきて、Background Agentも試しつつ、今はCursorとフォアグラウンドで会話をしながらのコーディングがメインになっている。

コミットとPR作成は自分でやってない

OSSだから英語でやっておきたいというのもあって、コミットやPRはほぼ自分では書いていない。Cursorに「差分をチェックしてその内容を元にコミットして」ってコミットをお願いしている。最初はCursor自体の生成機能(キラキラマークを押したらコミットメッセージを作ってくれるやつ)でやってみたりしてたけど、結局指示ファイルにコミットのときに気をつけることを書いておいてAgentに指示する方が楽でいいやってなった。

「じゃ、origin/mainとのファイルの差分をチェックしてPR出して」ってお願いして、PRを作ってもらってる。たとえばこんな感じ

github.com

コードレビューも自分ではやってない

コードレビューは、CursorのBugBotを使ってみていた。BugBotは、たぶん読み込む範囲が広いんじゃないかという気がする。レビュー完了までに少し時間がかかるけど、周りのコードを理解して指摘をしてくれる。

BugBotと同時に、GitHub Copilotにもコードレビューをお願いしていた。Copilotはレビューがはやい。でも、あんまり関係ないような指摘も、間違えた指摘もちょこちょこある。だから、BugBotでいいやーって気持ちになってた。のだけど、BugBotは7月末にトライアルが終わって有料になって月額$40はちょっと厳しいなぁ(Pro+ですでに月額$60払っている)という気持ちで、結局Copilotが残った。

みたいなことをつぶやいてたら「CodeRabbitどう?」って教えてもらったので、今はCodeRabbitも入れてみている。ちょっとレビューには時間がかかるけど、わりと指摘はいいなって感じがする。

コードレビュー後の修正も自分ではやってない

指摘をCursorのチャットに貼って「こう言われたんだけどどう思う?」って聞いて対応を決めて「じゃ、それでプッシュしておいて」ってお願いしている。

changesetもCursorに作ってもらっている

あ、あとchangesetもCursorに作ってもらっている。

Cursorにお願いしないところ、するところ

試行錯誤する部分は、僕が自分でやることが多い。特に「型をどう扱うか」という部分は、Cursorにお願いするわけにもいかないので、自分でやっている。

型が決まって方針が見えたら、あとはCursorにお願いできる。わりとミスは多いので、typecheckとLintは実行してもらっている。壁にぶつかりながら方向転換をしてゴールを目指しているような動き方をする。自動テストがあるともっといいんだろうけど、今はまだないからこのあと書いていかなきゃ。

追加で守ってもらいたいルールがあったときに、ESLintプラグイン自体をCursorに作ってもらったりもした。自分だけだとあんまり作る気にはならないけど、Cursorは喜んで作ってくれるので助かる。

ひととおりコードを書き終わったあとに、コードを元にドキュメント生成をしてもらったけど、これはそのままでは使えないなと思った。ただ、最初の足がかりにはなるので、それを元にCursorと会話をしながら「これいらない」「ここ分かりにくい」「ここは分割しよう」みたいに言って作っていった(のが最初に貼ったドキュメントのリンク)。

お金がかかる

いったん振り切ってしまおうと思ってやってたのもあって、結構お金がかかっている。正直、個人としては持続可能ではないなぁと思っている。

7月はゴリゴリコードを書いて$700くらい使ってしまった。claude-4-sonnetのthinkingをMaxモードで使っているからかなぁ。さすがにOpusはやめておいた。色々他のサブスクを解約したりしたけど、それでもちょっと使いすぎたなって思うので、今月からは色んなことを控えめにしたい気持ち。

gemini-2.5-proの方がリーズナブルだから、しばらく使ってみてたのだけど、gemini-2.5-proは前のめりすぎて、まだ考えてる段階なのにどんどん変更を加えていったり、型のエラーに悩み始めると暴走しやすいので、あんまり使わなくなった。

o3も値下がりをしていいなって感じなんだけど、普段のコーディングには自分の感覚とあんまり合わないなぁって思って普段使いはしていない。ただ、難しい設計を相談するのにはsonnetよりo3の方が頼りになるところある。「onResponseフックがいまいちしっくりこない」って相談したら「onRequestでdefer登録するのはどう?」って言われて「あー!それいいねー!」ってなったりした(実装した)。

  .onRequest((ctx) => {
    ctx.log().info('Request started', {
      requestId: ctx.req.requestId,
    });

    ctx.defer(() => {
      ctx.log().info('Request completed', {
        requestId: ctx.req.requestId,
        status: ctx.res.getStatus(),
      });
    });
  })

トータルではsonnetがバランス良くて、いろいろ手戻りとか暴走を止めたりするのを考えたら、いちばん楽。もうちょっと節約して使う方法を学ばないといけない。

ざっと書いた

という感じで、Cursorでコードを書いていて、自分の中にわりと感覚みたいなものが養われていっている感じはある。ひとりだと絶対にここまで作れなかっただろうから、生成AIすごいなぁという気持ち。ありがとうみんな。

だいたい欲しい機能は作ったので、こっからはひとつずつレビューして動作確認をしていく感じかなぁ。それと、パフォーマンス見ていかなきゃかな(たぶん遅い)。