今年はプログラマーの仕事はぐっと減らし、リモートで週2,3日稼働で可能な範囲で受けています。ちょうど、fluct時代にお世話になったもっちーさんが独立してさっそく困っているということで、今年はもちプロダクトのお手伝いをしてました。【PR】もっちーさんはお手伝いを募集中!
事業特性的にCloudflare環境(Worker + D1)を使うことになり、個人的には初めてまともにtypescriptでAPIを書きました。完全初心者でしたがAIもあるし人気スタックだからなんとかなるもんですね。このへん1年やってみての所感を放流しておきます。
いまts, hono, hono-rpc, zod, prisma, cloudflare worker, d1あたりのstackで開発しているが、まるで雰囲気でやっている(でもなんとなかっている
— トミール (@tomita) July 21, 2025
Cloudflare Workers
Cloudflareは以前から好き。SimpleというよりはEasyではあるのだがよくできてる。めんつゆ最高。謎のテクノロジーでsqliteがエッジで動く
DuckDBを使いたくて(Betaの)Cloudflare Containers使ってみた時は、npm run dev 叩いたら裏でdocker build/runが走ってすんなり使え、「おお、wrangler.jsoncにBindingを記述したらコンテナがDIされてきたぞ」と感心した。実稼働には、暖気設定とかがほしくなりそうではある
"containers": [
{
"class_name": "DuckDBContainer",
"image": "container/Dockerfile",
"max_instances": 10
}
],
Hono
yusukebe氏がめちゃがんばってる流行りのフレームワーク。さすが特に困ることなく使える😀 typescriptの型パズルをがんばってくれていてすごい
OpenAPI定義
@hono/zod-openapi と hono-openapi を使う2パターンがあって名前も似てて検索結果も混じりやすい。標準honoのapiに乗っかる方が好みなので、middlewareを提供するスタイルであるhono-openapiの方を利用した。
honoのapiはmiddleware/handlerが、next呼ぶか/returnするかの違いってのが綺麗なデザインだなと思うところ。
CIでopenapi.jsonを生成するのを入れてたが、ある時からなぜかdescribeRoute()の記述が取れないハンドラーがあった。調べたら「onError()があるsub app」で起きるエッジケースだったようなので、PRを送った。暫定的には、onErrorはsubで入れずmainでやるなどすれば良い
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から代入して)全部叩くみたいなテストのCIを入れた。
Prisma
型とスキーマ管理は欲しいし十分に高機能なので、まあ妥当な選択肢だったと思う。でもORMはやっぱ安易に自分の足元を撃ちますな。普通に使っててもすぐ波動拳になるし
...findUnique({
where: {
id: cid,
},
include: {
device_groups: {
select: {
group: {
include: {
devices: {
select: {
device: {
include: {
spot: {
include: {
owner_company: true,
},
},
},
},
},
},
},
},
},
},
},
...
$queryRaw に取り替えていこうか
Zod
有名なのと、prismaからのgenerator、hono-openapiも対応しているということで採用。しかし三方からがっちりロックインされたなあという印象がある。
実際、prisma, zod, hono-openapi, honoあたりは結局まとめてバージョンあげるがんばりが必要そうだ。かつ、作成されたtypeやclientなどを利用しているfront側アプリも全部。これはダルい
しかもprismaとzod最新版がupdateブロッカーになっていて辛い😂
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