今年はプログラマーの仕事はぐっと減らし、リモートで週2,3日稼働で可能な範囲で受けています。ちょうどfluct時代にお世話になったもっちーさんが独立してさっそく困っているということで、もちプロダクトのお手伝いをしてました。【PR】もっちーさんはお手伝いを募集中!
事業特性的にCloudflare環境(Worker + D1)を使うことになり、個人的には初めてまともにtypescriptでAPIを書きました。
いまts, hono, hono-rpc, zod, prisma, cloudflare worker, d1あたりのstackで開発しているが、まるで雰囲気でやっている(でもなんとなかっている
— トミール (@tomita) July 21, 2025
1年ほどやってみての所感を放流しておきます。
Cloudflare Workers
Cloudflareは以前から好き。SimpleというよりEasyではあるのだがよくできてる。めんつゆ最高。謎のテクノロジーでsqliteがエッジで動く
DuckDBを使いたくて(Betaの)Cloudflare Containers使ってみた時は、npm run dev 叩いたら裏でdocker build/runが走ってすんなり使え、「おお、wrangler.jsoncにBindingを記述したらコンテナがDIされてきたぞ」と感心した。1
"containers": [
{
"class_name": "DuckDBContainer",
"image": "container/Dockerfile",
"max_instances": 10
}
],
Hono
yusukebe氏がめちゃがんばってる流行りのフレームワーク。さすが特に困ることなく使える😀 typescriptの型パズルをがんばってくれていてすごい
OpenAPI定義をどちらでやるか
@hono/zod-openapi と hono-openapi を使う2パターンがあって名前も似てて検索結果も混じりやすい。標準honoのapi2 に乗っかる方が好みなので、middleware提供スタイルであるhono-openapiの方を利用した。
CIでopenapi.jsonを生成するのを入れてたが、ある時なぜかdescribeRoute()の記述が取れないハンドラーがあった。調べたら「onError()があるsub app」で起きるエッジケースだったようなので、PRを送った。
fix: resolve target handler marked as COMPOSED_HANDLER
https://github.com/rhinobase/hono-openapi/pull/204
入力はzodでバリデーションが走るとして、レスポンスについては
describeRoute({
validateResponse: process.env.NODE_ENV === 'test'
みたいにしておき、作成されたopenapi.jsonを元にGETを(必要パラメータはexamplesから代入して)全部叩くみたいなテストを入れた。
Prisma
型とスキーマ管理は欲しいしまあ妥当な選択肢だった… と思う。がORMはやっぱ自分の足元を撃ちますな。普通に使っててもすぐ波動拳になるし3
...findUnique({
where: {
id: cid,
},
include: {
device_groups: {
select: {
group: {
include: {
devices: {
select: {
device: {
include: {
spot: {
include: {
owner_company: true,
},
},
},
},
},
},
},
},
},
},
},
...
あと最新版がブロッカーになっていて辛い。そもそも、prisma, zod, hono-openapi, honoあたりは結局まとめてバージョンあげるがんばりが必要そう。かつ、作成されたtypeやclientなどを利用しているfront側アプリも全部。これはダルい
prisma generator
しかしprismaはジェネレーターできるのが良くて、protobufを懐かしく思い出した。今回、Active Recordっぽい動きをさせたいCRUDハンドラーばかりだったので、まるっとgenereate。jsでゴリゴリtsを書くのおもしろい
const fs = require('node:fs');
const path = require('node:path');
const {generatorHandler} = require('@prisma/generator-helper');
// npx prisma generate --generator handler
generatorHandler({
outputDir: './generated/handlers/',
onManifest() {
return {
prettyName: 'Hono Handler',
};
},
onGenerate(options) {
if (options.generator.output) {
this.outputDir = options.generator.output.value;
}
if (!fs.existsSync(this.outputDir)) {
fs.mkdirSync(this.outputDir, {recursive: true});
}
for (const model of options.dmmf.datamodel.models) {
...
DMMF構造体がもう少し(リレーションの情報とか)取りやすければ良いのだけど、モデル名・テーブル名・フィールド名だけでもけっこうやりたいことはできる
CIでのファイルの生成時(つまりcludflare workersとかwrangler devではない環境)ではcloudflare:workers moduleが読まれると動かないので、一部遅延読み込みにするなどをして対応したがもっといいやり方があるのかもしれない
const { getContainer } = await import('@cloudflare/containers')
generate祭り
実際こんな感じ
┌─▶ D1 migrations
│
schema.prisma ──┼─▶ prisma client
├─▶ ERD
├─▶ zod schemata / types
└─▶ handlers(hono) ────▶ openapi.json
└─▶ api/README.md
prisma generate部分はprisma.schemaでまとめて管理できるけど
generator client {
provider = "prisma-client-js"
output = "./generated/client/"
previewFeatures = ["driverAdapters"]
}
generator zod {
provider = "zod-prisma-types"
output = "./generated/zod/"
writeBarrelFiles = false
createInputTypes = false
}
generator handler {
provider = "node ./db/handler-generator.js"
output = "./generated/handlers/"
}
generator erd {
provider = "prisma-erd-generator"
output = "./README.md"
tableOnly = true
}
それ以外を順番に生成するのをpackage.jsonで管理するのに、npm-run-all2を使った
"scripts": {
...
"generate": "run-s generate:*",
"generate:cf-type": "wrangler types --env-interface CloudflareBindings",
"generate:schema-format": "prisma format",
"generate:schema-generates": "prisma generate",
"generate:api-openapi.json": "npx --yes tsx doc/openapi-generator.ts",
"generate:api-readme.md": "npx --yes openapi-to-md doc/openapi.json > doc/README.md"
},
D1 migrationをどちらでやるか
prismaを使う場合Cloudflare D1のマイグレーション管理のやり方は2パターンあるが、今回ターゲットがD1ということもありwranglerコマンドを使う方にした。
-
prisma migrateコマンド._prisma_migrationsテーブルで管理 -
wrangler d1 migrateコマンド.d1_migrationテーブルで管理
具体的には、schema.prismaをいい感じにしたあと、こんなので差分をmigrationファイルとして出力させて(必要なら微調整)
npx prisma migrate diff --from-local-d1 --to-schema-datamodel schema.prisma --script > db/migrations/0002_aaaaa.sql
ローカルで適用して確認
find .wrangler/state/v3/d1/miniflare-D1DatabaseObject/ -type f -name '*.sqlite' -delete
npx wrangler --env test d1 migrations apply test-db --local