目次
- 開発のきっかけ
- 予算の話
- 技術選定
- 開発での壁
- 現状と今後の目標
開発のきっかけ
私の所属する部活では車を乗り合わせて移動するため、独自の「配車管理サービス」が存在している。 最低限の機能については不満なく使えていたが、製作されたのが2012年ごろということもありUIが古く、刷新しようという話になった。
そこで現行システムのソースコードを見てみると、衝撃の階層分けとファイル名が......。
「これ、各ページのデータって信じられますか????」

ひとまずはUIの更新だけを行おうとコードを1つ1つ確認してみたところ、サービスの根幹はPukiWikiで、その上にPluginという形でシステムを形成しているようだった。極めつけはデータベースが独自形式で、「| (パイプ)」が1つずれるだけでそれ以降のデータが全て破損してしまう仕様で、実質いつ壊れてもおかしくない時限爆弾のようなシステムだったのです。
そこで、せっかくWebの技術もあるし、「数百人規模の利用が見込めるアプリを1から企画開発する機会も少ないから作ってみよう!」 となった。どうせ作るなら モダンな技術を使って、将来的に持続可能な構成にしたい と思い、開発をスタートしました。
予算の話
さて、開発には予算がつきものということで部活の幹部に聞いてみたところ、
「今使ってるサーバーならそのまま使ってもいいけど、別で予算は出せないよ」
とのこと。世知辛いね。
そこで今使っているレンタルサーバー(Xserver)の使用を確認してみるとperl、rubyかPHP、Python2.7 / 3.6とMySQL5.7のみ使用できる環境だった。
そこで一旦はPython3.6とMySQL、GraphQLを用いてAPIサーバーを作成したがさまざまな制約から応答速度が avg.1000ms と全くもって実用に耐えるものではなかった。
そこで 「無料」かつ「高速動作可能」なDBと実行環境を探す必要があった。
そんな折、見つけたサービスはCloudflare WorkersとD1である。月間アクティブユーザーが100名程度のサービスは無料枠で収まりそうだということでこれが候補に上がった。
試しに簡単なDBとAPIを作成したところ応答速度が10ms程度であったため、これを採用する事にした。
技術選定
さて、技術選定ということで実行環境の制約を確認する必要がある。
Cloudflare WorkersはChromeのJSエンジンであるV8を使用しているので言語はJavaScriptないしTypeScriptで、D1はSQLiteである。この制約の下、技術スタックを決めていく。
また、将来的に持続可能な(メンテナンスしやすい)サービスを目指して、開発コミュニティが活発な以下のスタックを選定した。
| 役割 | パッケージ名 |
|---|---|
| UI/UX | React Router v7 |
| スタイリング | Tailwind CSS |
| 認証 | JWT |
| APIルーティング | Hono |
| ORM | Drizzle ORM |
| バリテーション | zod |
開発コミュニティの大きさという観点ではORMはPrisma+Pothosを使用すべきであるが、後述の理由により採用を見送った。
開発での壁
開発の順序は以下の通り
- DB設計と構築
- ORM設定
- バリデーターの設定
- APIルートの設計
- 認証ルートの作成
- UI/UXの作成
DB構築
開発を進めていくにあたって最初にDBの設計を始めた。
Qiitaの制約によりリレーションの線の数が省略されている。
ORM設定
このDB設計をもとにORMを設定していくのだが、
初期設定時にリレーション先の存在しないテーブルを参照する必要があり、テーブル間で参照の輪ができてしまう本環境ではPrisma + Pothosの解決できなかった。
なんでなんだろうね。
記憶が曖昧だが、Pothos側がエラーを吐き出していたような記憶がある。
そのため、Drizzle ORMを使用することにした。
バリデーターの設定
そして次にバリデーターのzodをセットアップした。ここはセキュリティに気をつけながら実装するあまり、存在しないカラムをomitしていることに気づかず以下のようなエラーに困らされた。
✘ [ERROR] Error: Unrecognized key: "hashPassword"
キャメルケースとスネークケースの違いや、タイポには気をつけましょう......。
APIルートの設計
APIルートは軽量かつ高速なHonoを利用することにした。この時にDrizzleとbackend, frontendでモノレポ構造にすることを思い立ち、これが後の困難を招くとはつゆ知らずモノレポ構造に挑戦した。
APIルートは基本的に「1テーブルに1エンドポイント」の構造とし、各ルートにCRUDや要素の絞り込み機能を実装し、完成とした。
認証ルートの作成
DBには個人情報や勝手に書き換えられたくない情報も多いのでメソッドごとに認証の必要有無を設定する必要があった。この時にHonoのMiddlewareがとても役立った。bcryptを用いてパスワードをハッシュ化すると同時にJWTを用いてアクセス認証を行う設計とし、作成した。
UI/UXの作成
これが最も難関だったと言っても過言ではない。
まずビルドが通らないのだ。Cloudflare公式のテンプレートを用いてもReact-router公式のテンプレートを用いてもビルドが通らず困った。さまざまなZennやQiita、RedditやGitHubを見漁っても解決しない。最終的に、APIルート作成に導入した モノレポ構造を一部止める(構成を見直す) ことで解決しました。
そしてHonoには型安全を強力に推し進めるhcという機能があるのだが、これもまた沼った。まず作例に多い
import { hc } from "hono/client"
import app from "app/api"
type AppType = typeof app
export const loader = () => {
const client = hc<AppType>("https://****.com/api/***")
}
ではうまく通信ができなかった。
デバッグを見てみるとWorkerが自分自身を叩きに行って無限ループに陥り、ハングアップしていたようである。
そこで、
import { hc } from "hono/client"
import app from "app/api"
type AppType = typeof app
export const loader = async ({ context }: Route.LoaderArgs) => {
const env = context.cloudflare.env;
const client = hc<AppType>("http://localhost", {
fetch: (input: RequestInfo | URL, init?: RequestInit) => {
return app.request(input.toString(), init, env);
},
});
const res = await client.*****.$get();
return await res.json();
};
としてみたが、contextが存在しないとしてエラーになった。
結果としてはページ側に
// 修正前
const Page() => {}
// 修正後
const Page({ loaderData }: Route.ComponentProps)) => {}
{ loaderData }: Route.ComponentProps)が抜けていただけだったが、Gemini 2.0には指摘できなかったようだ。
この解決に丸2日持って行かれた。Gemini 3.0よもう少し早くリリースしておくれ。(とはいえ、fetch をオーバーライドして app.request を直接呼ぶという解決策自体は正解だった)
現状と今後の目標
さて、そんなこんなであとはUIを設計してリリースするだけとなったわけだが、やる気がだいぶ削がれてきてしまっている😇
そこでUIやる気マックスの後輩くんを召喚して進めていく事にした。
TimeTreeのUI/UXはとても素晴らしいので丸パクリ参考にさせていただく事にした。
完成した日にはGitHubのリポジトリも公開しておこうかと思う。