HonoとKyselyでEnd-to-end typesafeな開発体験を!
はじめに
こちらはe-dash advent calendar 2024の7日目の記事です。
技術選定のきっかけ
弊社ではこれまでお客様の請求書情報の登録等を行う管理画面をノーコードアプリで利用していました。
しかし、画面によっては表示できない or 1~2分かかるといった事象があり、カスタマーサクセス(CS)
の業務に支障が出て、そろそろ限界に達していました。
そこで、管理画面の刷新プロジェクトがstartしました。
カスタマーサクセスの課題解決にあたり、どのような制約や要望があったのか?
これまでノーコードで実行できていた管理機能(シンプルなCRUDがメイン)なので、言語による制約はありませんでした。
新管理画面への要望についても、特定の言語しかできないことはなさそうでした。
アプリ本体のDBを参照するので、DB Firstな開発は制約の1つでした。
開発ではどんな課題があったのか?
e-dashではこれまでバックエンドにGo言語、API設計にはREST APIを採用してきました。
API定義にはswaggerを利用していますが、API定義が仕様を満たすかという観点に加えて、以下のような点でもみる必要があり辛いという意見がエンジニアメンバーから特に多かったです。
- yamlのlintが効くの?
- 型生成時に実装への影響は?
- yamlの命名・記法の統一は他とあってる?
- swaggerと実体はあってる?
- etc...
2つ目がgormです。GoのORMとしては有名ですが、genを使ってないため、table名やカラムへのインテリセンスが効かないというのが、辛いpointでした。
※今回話さないこと
フロントエンドの技術にも様々な観点で議論しましたが、今回は省略します。
この記事では、バックエンドの技術選定(HonoとKysely)を中心にお話しします。
技術選定の結果と理由
最終的に以下の技術スタックに決まりました。
言語:Typescript
FW: Hono, Remix
Query Builder: Kysely
議論したこと
様々な観点でメンバー同士で話し合ったことを質疑応答の形式で記載しました👇
管理画面でGoである必要はあるのか?
管理画面にGoほどのパフォーマンスが必要になるケースはなさそう。
※弊社でほとんどのバックエンドがGoで書かれてるため。
フロントエンド・バックエンド両方をtypescriptにすると、採用面でもエンジニアの募集がしやすいのでは?
Yes。また、社内でフロントエンドエンジニアがバックエンドに挑戦したいとなった時(その逆も)、ハードルが下がりそう。
フロントエンド・バックエンド両方をtypescriptにすれば、swaggerの問題をtRPCやHonoで解決できるのでは?
routerに書いたことがそのままAPI定義になるので、よさそう。
tRPCとHonoが候補に上がったが、違いは?
Honoはweb標準にも準拠してて、様々なRuntimeで動く。hono提供のpackageで、routerに記載した内容からopenAPIに出力することが可能。RESTを意識する。
tRPCは、adapterはweb標準の記載があったが本体は見つからなかった。openAPIの出力も同様に見つけられなかった。RPCを意識する。
Goを残した場合、swaggerの問題を解決するにはどんな方法があるか?
TypeSpecというツールでTypeScriptっぽくswaggerを記載できる。copilotに任せることもできるが、間違うこともある。swaggerの課題感は解決できそうだが、tRPCやHonoの方が開発者体験として良さそうとなり、Goは外れた。
typescriptで、ORM/Query Builderはどうする?
- sqlc-gen-typescriptはまだearly-accessで破壊的変更が怖いし、動的なクエリの作成(主に検索)には弱いため、除外した。
- prismaでDB Firstな開発もできるが、prismaの機能を存分に利用できない。そのため、PSLなどのprismaの学習コストが高くつくと考えた。
- Kyselyは学習コストが少なく、type safeなSQLを直感的に作成しやすい。DBからmodelを生成できることはもちろん、生成したmodelをキャメルケースにするpluginもあるので、アプリ側(キャメルケース)<=> DB(スネークケース)のマッピングをKysely側がやってくれる点も良かったため、採用した。
管理画面を担当するメンバーで、Typescriptでバックエンド開発経験のあるメンバーはいるのか?(=言語に対する学習コストは?)
自分含めバックエンドメンバーの何名かがExpress、NestJSの開発経験があるので、問題なさそう。
脱炭素を加速する!CO2排出量を可視化する!と謳ってる弊社e-dashで、燃えそうなHonoを導入して良いのか?
なるほど、確かにー。(棒)
このように議論し、上記の結果となりました。
導入後・・・
良かった点
- routingの仮実装をすれば、そのままAPI定義としてフロントエンドにレビューしてもらえるため、認識の齟齬が少ない。レビュー指摘を受けても修正もしやすい。(Hono)
- 例えば、以下のように
/todo/:id
のAPIをRouterに実装して、Dummyの値だけ返しておけば、フロントエンドにレビューしてもらえますし、実際にAPIを叩いて実装を進めることもできます。
- 例えば、以下のように
# sample
const route = app
.get(
"/todo/:id",
zValidator(
"param",
z.object({
id: z.string().max(10),
})
),
(c) => {
const dummy = {
id: "1",
title: "dummy",
completed: false,
};
return c.json(dummy);
}
)
- 同期的に型検証されるので、API定義のミスをすぐ検知できる(Hono)
- 型生成の1stepがなくなった(Hono)
- 型のインテリセンスが効くようになったのでクエリがかきやすくなった(Kysely)
- aliasが簡単にふれる。また、aliasに対しても型補完も効く点も良い(Kysely)
- camel case pluginを利用することで、実装側(キャメルケース)とtabel名(スネークケース)の形式違いをKyselyが生成するModelで吸収できる点(Kysely)
導入することで新たに感じた課題感
- 同期的に型検証されるゆえに、一括でフロントエンド,バックエンドの修正が必要な点。(Hono)
- swaggerでAPIを変更後に実装側で反映させるまで、フロントエンド・バックエンドで疎通がうまくいかないことに比べたら楽だと思います。
- 影響が大きい場合は、移行用のpropertyを入れてから古いpropertyを削除するといった対応が必要かもしれません。
- 204を返したい場合は標準のResponseで返さないといけない。issue(Hono)
- Documentがもう少し充実してると嬉しい(Kysely)
- table名、column名を””で囲うのがちょっと面倒。※Prismaだとdb.table名で書けるため(Kysely)
まとめ
HonoとKyselyを採用することで、フロントエンドからデータベースまで一貫した型安全な開発が可能となりました。
導入することで新たに感じた課題が出たものの、以前よりも良い開発者体験を実現しています。
今回のように積極的に新しい技術を導入していき、開発効率の向上に向き合っていきたいですね。
Discussion