はじめに
本が好きでよく本を買うのですが、だんだん置き場所がなくなってきたのもあり、ずっと欲しいと思っていた壁一面の本棚を作りました。すると、自分の持っている本を管理するための仕組みが欲しいと思うようになり、蔵書管理のための Web アプリケーションの開発を進めています。
(余談ですが、本棚の設計・組み立てにあたってはこのサイトとこのサイトに感化された先人たちの知見に助けられました・・・笑)
さて、Web アプリの開発にあたっては、Claude Code の力を存分に借りながら進めているのですが、
- Google Apps Script + Spreadsheet
の非常に簡素な構造から、
- Next.js (Vercel) + Hono (Cloud Run) + PostgreSQL (Supabase)
というモダンな構成にリアーキテクチャを進めてきました。
本記事では、Web アプリケーション開発の経験のなかった筆者が、アーキテクチャをどのように進化させてきたか、そして Claude Code をどのように活用してきたかについてまとめます。
アーキテクチャの変遷
アーキテクチャは、以下の 3 段階で進化していきました。プロトタイプとして動くものをベースに進めていく Google Apps Script ベースのところから、ちゃんとした(はず・・・ご助言あればもらえると嬉しいです!)設計まで移行しています。
Phase 1: Vanilla JS 1 ファイル時代
アーキテクチャ
最初は Google Apps Script + Google Spreadsheet で作りました。
フロントエンドは Google Apps Script の HtmlService で返す HTML テンプレートに Vanilla JS をべた書き。サーバーサイドも Google Apps Script の関数を 1 ファイルにまとめた構成です。データストアは Spreadsheet をそのまま DB 代わりに使っていました。デプロイは clasp を使っています。
Google Apps Script + Spreadsheet を選んだ理由
昔から Google Apps Script をよく使っていたのもあり、開発には慣れていたのと(例えば、「S3 Vectorsによる激安RAG環境をデータパイプラインに組み込もうとしてみる」など)、検討事項の多いアーキテクチャ設計や運用を考えずにすぐ作り始められるのが大きかったです。
- Google のインフラに乗っかるのでホスティング費用ゼロ
- Google の認証を簡単に利用することができる
- Google 系のサービスとのつなぎこみが非常に簡単にできる
- Spreadsheet がそのまま管理画面にもなる(データを直接見られる)
- デプロイは clasp push 一発
しかしコードが増えるにつれて、1 ファイルでは見通しが悪くなり、Claude Code が生成したコードのレビューが辛くなってきました。なかなかテストも書きにくい状態だったので、コードの変更に伴う影響の考慮もしにくくなってきたという課題が出てきました。
Phase 2: TypeScript 化 + ファイル分割 + テスト導入
移行のきっかけと構成
先述の通り、Phase 1 の 1 ファイル構成ではコード全体の見通しが悪くなってきたことと、テストをしっかり入れたくなってきたことが移行の動機です。
ただ、Google Apps Script はそのままだとファイル分割が難しい環境です。通常の JavaScript / TypeScript では import / export を使ってファイルを分けますが、Google Apps Script のランタイムはこの仕組み(ES Modules や CommonJS)をサポートしていません。
Google Apps Script が認識するのはグローバルスコープに定義された関数だけなので、「このファイルからあの関数を読み込む」ということができないのです。
そのとき、 esbuild というバンドルツールを知りました。esbuild は複数の TypeScript ファイルを 1 つの JavaScript ファイルにまとめてくれます。出力形式を IIFE (即時実行関数式) にすると、Google Apps Script が要求する「グローバル関数が並んだ 1 ファイル」の形にしてくれます。
要するに、ローカル開発ではディレクトリ・ファイルを自由に分割して開発し、ビルドで 1 ファイルにまとめたものを clasp で Google Apps Script に push するという構成です。
ファイルが分割できることで、テストを十分に書くことができるようになります。テストは Vitest を使い、Google Apps Script 固有の API(SpreadsheetApp、UrlFetchApp など)はすべてモックして Node.js 環境で回せるようにしました。
なお、この構成にするにあたっては、以下の記事を参考にさせてもらいました。
この構成で得られたこと
- ファイル分割でコードの見通しが良くなった
- リントや型チェック、テストを自動で回せるようになり、コードを書きながらのフィードバックが回りやすくなった
- Claude Code が生成したコードのレビューがしやすくなった
それでも残った課題
Phase 2 で Google Apps Script としてのコード品質は上がりましたが、プロトタイプとしての開発を通じて要件がある程度明確になってきた一方で、根本的な課題が残っていました。
- レスポンスが致命的に遅い(Spreadsheet の全走査 + Google Apps Script の実行オーバーヘッド)
- Spreadsheet でのデータ管理の限界(データの整合性を担保しにくい)
- CSS が機能追加のたびに無限に増えていく(グローバル CSS の管理が破綻しかけていた)
もともと動くものを作ることを優先していたので、一定破綻をすることは想定通りでした。ただ、ここまでの開発を通して一定の要件が見極められてきたことと、いい加減遅すぎるレスポンスにイライラしてきたのもあり、モダンなアーキテクチャに移行することを決めました。
なお、そもそもアプリケーション DB の設計をきちんとやるのが初めてだったのもあり、その後に試行錯誤しながら苦労したのを考えると、初めからモダンな設計はやろうとしてもできなかったなと思っています。要件への理解が深まるからこそ、適切な DB 設計ができるのだと。データ基盤におけるデータモデリングも同じですけどね。
Phase 3: Web アプリとして再設計
ということで、Google Apps Script + Spreadsheet の制約を突破するために、Web アプリケーションとしてゼロから再設計しました。Google Apps Script 時代も設計書は書いていましたが、今回は本格的な Web アプリケーションとして作るため、システム構成・DB・API・画面・運用まで 8 本の設計書を作成してから実装に入っています。
| No | 設計書 | 内容 |
|---|---|---|
| 1 | overview | 目的・非目的・前提条件・非機能要件 |
| 2 | architecture | システム構成・技術選定・認証・セキュリティ |
| 3 | domain | 用語整理・ユースケース・機能一覧・権限設計 |
| 4 | database | ER図・テーブル定義・RLS |
| 5 | api_spec | API詳細仕様 |
| 6 | ui_design | 画面遷移・レイアウト・操作 |
| 7 | operation | 環境構成・CI/CD・リリース手順 |
| 8 | guidelines | 設計方針・開発規約・プロジェクト構成 |
設計書を作る過程では Claude Code とドキュメントベースで対話を重ね、技術的な知見を深めながら進めていきました。
技術選定
技術選定では「低コストで運用できること」「利用が多く学習価値が高いこと」「セキュリティを担保できること」の 3 点を重視しています。
フロントエンド: Next.js (App Router)
フロントエンドは Next.js の App Router を採用し、Vercel にホスティングしています。スタイリングは Tailwind CSS + CVA (Class Variance Authority) です。Phase 2 で CSS が無限に増えていく問題があったので、Tailwind でユーティリティクラスに統一しつつ、CVA でバリアント(サイズ・色・状態の組み合わせ)を型安全に管理しています。
UI コンポーネントには react-aria を使っていて、アイコンには lucide-react、サーバー状態の管理は TanStack Query です。
このあたりはモダンなアプリケーション開発ではよく使われるものとして選定していますが、いかんせんフロントエンドの開発経験は皆無だったので、技術選定後はまず本を読むことからはじめました。AI と対話することも勉強になりますが、まとまった情報を本から学習することは相変わらず大事ですね。
なお、TypeScript は検証目的で MCP の開発をしていたときに使っていたので、その経験も踏まえつつになっています。
API: Hono を Next.js と分離
Next.js だけでバックエンドを完結させるのもできますが、あえて Hono で API サーバーを分離しました。一番の理由はセキュリティです。Supabase をフロントエンドから直接操作する構成では、セキュリティリスクがあるという話を Twitter 上で多く見かけたため、DB へのアクセスを API 層からのみに限定することで、堅牢な仕組みにしたいと考えました。
ランタイムは Bun で、Cloud Run にデプロイしています。コンテナの配置場所としては、ECS と Cloud Run のいずれでもよかったのですが、Cloud Run の方が使い慣れていることと、設定が簡単らしい(深堀りはしていませんが)ということで、そちらを選定しています。
DB: PostgreSQL (Supabase)
最初は全体構成を Firebase に寄せる形で考えていました。しかし、Firebase の Firestore だと NoSQL であるため、JOIN やサブクエリが使えず、テーブル間を跨いだ柔軟な検索が難しいことがわかり、PostgreSQL を選択しました。
Supabase を選んだのは、昨今の個人開発でよく使われていること、無料枠である程度使えること、Row Level Security (RLS) で DB 層のアクセス制御ができること、SQL 自体の学習価値が高いことが理由です。
何も指示せずに AI に開発をさせると生 SQL を書き始めたりするのですが、いやそれは勘弁して・・・ということで ORM として Drizzle を導入し、クエリは Drizzle 経由を必須にしています。
認証: Supabase Auth (Google OAuth)
認証は Supabase Auth で Google OAuth を使っています。ID/パスワードの認証はセキュリティ上あまり使いたくないため、ひとまずすべて Google 認証に寄せています。
またこのアプリは友人限定で使っているため、誰でもサインアップできる状態にはしたくありませんでした。Supabase Auth 自体にはサインアップを特定のメールアドレスに制限する機能がないため、PostgreSQL の BEFORE INSERT トリガーを使って制御しています。auth.users テーブルにレコードが挿入される前に、アプリ側の users テーブルにそのメールアドレスが登録済みかを確認し、未登録であればサインアップを拒否する仕組みです。
加えて、Supabase に直接依存しないよう抽象化しています。認証トークンの検証は Supabase クライアントではなく jose ライブラリで JWKS を取得して行い、DB アクセスも Drizzle ORM 経由です。将来的に別のサービスに切り替えることになっても、影響範囲を限定できるようにしています。
多層防御のセキュリティ設計
アーキテクチャで一番こだわったのはセキュリティの多層防御です。Google Apps Script 時代は「Google Apps Script の認証に通ればすべてのデータにアクセスできる」という単層構造でした。新アーキテクチャでは、フロントエンド・API・DB の 3 層で防御しています。
ポイントは API 層と DB 層の連携です。API 層で withUserContext() を呼んで PostgreSQL のセッション変数(app.user_id)をセットし、RLS ポリシーがそれを参照します。仮に API 層にバグがあっても、DB 層が別ユーザーのデータへのアクセスを防いでくれます。
DB 接続も用途ごとに分けていて、データ操作用(CRUD 全般)とロール取得用(users テーブルの SELECT のみ)で別々の DB ユーザーを使っています。
そもそもの利用者を限定し、認証しないと API リクエストが実施できず、API から DB へも RLS を利かせることで制限する形で、自分でも実装をしながらなるほどこうやってセキュリティを担保するのかと勉強になりました。
しっかりと作りこむときには DB アクセスをインターネットから分離することは当然だという理解はしていましたが、インターネット経由でも認証+ RLS の制限によって一定のセキュリティを担保できるというのがよく理解できました。
監視・分析
単に使ってみたかったからという動機が大きいですが、監視・分析基盤も入れています。
- エラー監視: Sentry(フロントエンドは
@sentry/nextjs、API は@sentry/bun) - アクセス分析: GTM + GA4 + Microsoft Clarity(ヒートマップ・セッションリプレイ)
- パフォーマンス: Vercel Speed Insights
エラーやアクセスにはユーザー ID を渡すことで、より監視・分析が有効にできるような形にしています。
開発プロセス
ここからはアーキテクチャの話を離れて、設計から検証・デプロイまでの開発プロセスについてまとめます。
設計
「設計書を先に書いてからコードを書く」フローを徹底しています。まず Claude Code Web で技術調査や方針検討を行い、その対話の中でまとまったドキュメントを draft として保存します。この draft をもとに Claude Code で正式な設計書に起こし、v1 → レビュー → vN とバージョンを重ねていきます。Phase 3 からはカスタムエージェントによる AI レビューも組み込みました。
v1 を必ず人間が見るルールにしているのは、方向性がずれたまま精緻化されるのを防ぐためです。
開発
実装は t-wada の TDD (Red → Green → Refactor) で進めています。テストを 1 つ書いて失敗させ、ハードコードで通して、テストを追加しながら一般化する。CLAUDE.md にこのルールを書いておくと、指示しなくても自然にこのサイクルで動いてくれます。
そのほか開発時の Claude Code の使い方についても色々と工夫をしているのですが、詳細については後述します。
検証・デプロイ
GitHub Flow ベースで運用しています。
テストは単体・統合・E2E の 3 層で構成しています。単体テストは Stop Hooks と CI の両方で実行されます。統合テスト・E2E テストは DB マイグレーションが必要なため開発フローのなかで Claude Code がコマンドを実行して対応しています。E2E テストには Playwright CLI (Skills) を使っており、UI 変更時にも Playwright で認証済み状態の画面を確認しながら調整を行っています。
リモートに Push すると Vercel のプレビュー環境が立ち上がり、実データで動作確認ができます(本来は検証用 DB を別に立てるべきですが、ブランチ戦略が複雑になるためまだそこまでやっていません)。PR を出すと単体テスト・Docker ビルド検証・Terraform plan が走り、main マージで自動デプロイされます。
Terraform の plan / apply は tfaction で管理しており、plan 結果が PR にコメントとして投稿されます。Google Cloud への認証は Workload Identity Federation を使い、サービスアカウントキーを持たない構成にしています。Format・Lint・型チェックは Hooks のほかにも pre-commit でも実行しており、人間・AI 問わず担保されているため、CI からは除外しています。
仕様ドキュメントの自動生成
実装と仕様書が乖離するのを防ぐために、コードから仕様ドキュメントを自動生成しています。
- API 仕様:
@hono/zod-openapiで Zod スキーマから OpenAPI (Swagger) 定義を自動生成 - DB スキーマ: Drizzle ORM のスキーマ定義から DBML を自動生成し、ER 図を常に最新の状態で出力
どちらも「コードが正」であり、ドキュメントはそこから生成されるものという位置づけです。更新を忘れないよう、CLAUDE.md の開発フローにドキュメント生成を組み込み、コードレビューエージェントの観点にも「仕様ドキュメントの更新漏れがないか」を含めています。
Claude Code のカスタマイズ
アプリケーションを開発していくこともそうですが、アプリケーション開発を通して Claude Code のカスタマイズも着々と進めていっています。
細かいプラクティスはありますが、重要なポイントとしてはフィードバックサイクルを短く回すことと、強制の構造を調整することだと考えています。
例えば、
- やっていいこと・やってはいけないことをAllow / Deny リストで整備しておく
- Format, Lint, Typecheck は PostToolUse でスクリプトを実行させることで、小さいサイクルで決定的に実現されるようにする
- ある程度小規模で機械的に処理できる指摘事項は PreToolUse で Deny する
- パッケージマネージャーを bun にしているのに npm を使いたがるので、これは Block して bun を使えと指示している
- package.json のパッケージ名をそのまま Edit したがるので、Block して install させるようにしている
- 決定的にはできないが遵守するべきことを、メモリファイルや Rules に読み取りコンテキストを調整できる形で指定する
- ただ、これだけでは遵守から漏れることがあるので、サブエージェントを使って指定している規則から逸脱しないように指摘を入れる
といったことです。
特にサブエージェントを使ったルール順守のレビューは非常に効果が高かったです。導入前はただ動けばいいくらいに実装されていることが多かったところから、既存コードの規則性の準拠や保守性、将来的な拡張性を考慮した形に改善されていくのが効果的でした。
具体的な内容は以下の通りです。
CLAUDE.md とルールファイル
プロジェクトルートと各パッケージに CLAUDE.md を配置して、Claude Code がそのディレクトリで作業するときに必要な知識を自動で読み込めるようにしています。
さらに .claude/rules/ にファイルパターンごとのルールを置いていて、テストファイルを触っているときはテスト規約が、TypeScript を書いているときは TypeScript の規約が自動でコンテキストに入ります。
たとえば TypeScript のルールファイルでは、以下のような規約を定義しています。
-
any禁止(型を明示するか推論に任せる) -
asによる型アサーション禁止(型ガード・satisfies・ジェネリクスで代替) - 非 null 断言 (
!) 禁止(条件分岐または optional chaining で安全にアクセス) -
@ts-ignore/@ts-expect-error禁止(型定義の修正で対応) -
eslint-disableは原則禁止(やむを得ない場合は理由コメント必須) - 命名規則の統一(ファイルは kebab-case、変数は camelCase、型は PascalCase など)
Claude Code はこれらのルールを読み込んだ上でコードを生成するので、最初から規約に沿ったコードが出てきます。仮に逸脱しても PostToolUse の ESLint・型チェックで即座に検出されます。
Hooks
Claude Code の Hooks 機能で、品質チェックの自動化と「やってはいけないこと」のブロックを仕込んでいます。
| Hook | タイミング | 内容 |
|---|---|---|
| PreToolUse | コマンド実行前 | npm/npx をブロック(Bun に統一) |
| PreToolUse | コマンド実行前 | 本番 DB 接続をブロック |
| PreToolUse | ファイル編集前 | dependencies 直接編集をブロック(bun install を使わせる) |
| PostToolUse | ファイル書き込み後 | Format → Lint → TypeCheck を順番に実行 |
| Stop | 会話終了時 | 単体テスト実行 |
Claude Code が規約を破るコードを書いた瞬間にフィードバックが返るので、手戻りが減りました。
カスタムエージェント
エージェント機能で、コードレビュー・設計書レビュー・DB マイグレーションレビュー・コーディング規約チェックの 4 種類のレビュアーを作っています。
CLAUDE.md の開発フローにコードレビューのステップを定義しているので、実装が完了すると Claude Code が自動的にレビューエージェントを起動してくれます。手動でコマンドを打つ必要はありません。
レビューを学習に活かすレスポンス設計
カスタムエージェントで工夫しているのが、サブエージェントからメインエージェントへのレスポンス形式を定義しているところです。レビュー結果をまとめて渡すのではなく、指摘事項を 1 件ずつ個別に解説し、対応案を推奨度付きで提示し、1 件ずつ確認を取るようにしています。
これは単にレビューを自動化したいだけではなく、自分自身の学習のためにやっています。セキュリティ・パフォーマンス・設計パターンといった異なる観点からのレビューを 1 件ずつ確認することで、「なぜそれが問題なのか」「どういう選択肢があるのか」を毎回理解しながら進められます。Web アプリ開発の初心者にとって、コードを書きながら体系的に学べる仕組みになっています。
また、このレビューサイクルがあることで、試行錯誤の過程で書いた「とりあえず動く」コードに対しても改善のフィードバックが回ります。たとえば重複したロジックの共通化や、セキュリティ観点での批判的な指摘(入力バリデーションの漏れ、エラーメッセージへの内部情報の露出など)が実際に出てきて、それに対応していく中でコードの品質が上がっていきます。
おわりに
Web アプリケーション開発の経験がない状態から、Google Apps Script + Spreadsheet の 1 ファイル構成 → TypeScript + ファイル分割 + テスト → Next.js + Hono + PostgreSQL のモノレポという 3 段階を経てきました。
振り返ると、各フェーズにはそれぞれ意味がありました。Phase 1 で「まず動くものを作る」ことで要件が明確になり、Phase 2 で TypeScript とテストを入れたことでコードの品質を担保する感覚がつかめました。そして Phase 3 で本格的な Web アプリケーションとして再設計する際には、Phase 1・2 で得た理解があったからこそ、設計判断に自信を持てました。
この開発を通じて強く感じたのは、Format・Lint・型チェック・ユニットテスト・統合テスト・CI/CD といった、エンジニアにとって当たり前のことを当たり前にやれる運用に組み込むことの重要性です。
もう一つ重要だと感じたのは、AI の出力を鵜呑みにしないことです。Claude Code は強力ですが、適当なことを言うことも少なくありません。AI にコードを書いてもらえる時代でも、設計判断ができないとまともなものは作れないという実感があります。
今回 Skills やサブエージェントを取り入れながらなるほどと思っていたところ、Teams が導入されて動きが早いのには困ったものですが、エンジニアとしての知見を急速に高められる環境が整ったということで、上手く生かしていきたいと思います。