FLARES LLC

Technical document

TypeScriptで動かなくなった実行基盤を、Go・エッジ・Single SPAへ移す設計

今回の移行理由は、TypeScriptという言語が悪いからではありません。サーバー側のTypeScript実行系が増え、実行プロセス、サービス境界、生成物、デプロイ手順が分散した結果、変更したコードを同じ前提で動かし続けることが難しくなりました。ブラウザ側のTypeScriptは残し、実行基盤だけをGo中心へ整理することで、動作条件と責務の境界を小さくします。
アーキテクチャ / 移行設計約16分公開日 2026年8月12日更新日 2026年8月12日
分散した実行系をGo、エッジ、Single SPAの構成へ整理する技術者のイラスト

Summary

この文書の要点

  • TypeScriptを廃止するのではなく、ブラウザ側のTypeScriptとサーバー側の実行基盤を分けて判断します。
  • Go modular monolith、Cloudflare Pages Functions、Cloud Run、Cloud SQLで、実行境界を少数の責務へ集約します。
  • Hostからテナントを解決する処理はエッジで行い、バックエンドとDBでも必ず検証してデータ分離を二重化します。
  • Single SPAとProtobufを組み合わせ、画面とAPIの増加がサービス数と手作業の増加に直結しない構成を目指します。

移行の出発点は『TypeScriptが動かない』という運用上の事実です

TypeScriptは、ブラウザのUI、サーバーAPI、バッチ、開発ツール、CIの補助処理まで同じ言語で書ける便利さがあります。一方で、サーバー側の実行系が増えると、各プロセスが依存するランタイム、生成物、環境変数、起動方法、ヘルスチェックを個別にそろえる必要があります。コードが型チェックを通ることと、配備先で安定して動くことは別の条件です。

今回の移行では、TypeScriptで書かれたサーバー側の実行系が、増えたサービス境界と運用手順の組み合わせによって動かし続けにくくなったことを出発点にしました。『TypeScriptを使わない』と決めるのではなく、どの処理をどのランタイムが責任を持つかを引き直します。ブラウザで動くUIはTypeScriptを使い続け、API、データアクセス、常駐処理の中心をGoへ寄せます。

この区別をしないまま全面書き換えを始めると、言語の置き換えだけが目的になります。先に、動作しない原因がコードのバグなのか、実行環境の不一致なのか、サービス間の契約のずれなのかを分けます。そのうえで、再発しやすい実行境界を減らすことを移行の目的にします。

  • 型チェックの成功と、本番ランタイムでの起動・接続・終了の成功を分けて検証する。
  • 言語の好き嫌いではなく、実行プロセスと運用境界の数で問題を捉える。
  • ブラウザ側のTypeScriptと、サーバー側のランタイム選択を別々に決める。

サーバー側はGoの単一バイナリへ集約し、フロントはTypeScriptを残します

サーバー側の目標は、複数の実行系をGo modular monolithへ集約することです。HTTPルーティング、認証、テナント解決、ドメイン処理、外部連携を一つのプロセス内で責務分離し、ビルド成果物と起動条件を少なくします。モジュールを分けることと、サービスを分けることを同一視しないのが重要です。

Goを選ぶ理由は、サーバーコードを一つの静的バイナリとして扱いやすく、Cloud Runの起動・終了、メモリ使用量、同時実行の計測をそろえやすいからです。すべてを一つの巨大なパッケージへ詰め込むのではなく、依存方向を `apps → addons → packages` のように固定し、ドメイン単位の内部境界を維持します。

一方、UIはReactとViteを使うSingle SPAとしてTypeScriptを残します。APIの契約をProtobufから生成し、ブラウザ側では型とReact Queryのフックを受け取るため、TypeScriptの静的な恩恵は維持できます。『サーバーをGoにしたからフロントも別言語にする』という連鎖を避け、変更の目的に関係する境界だけを移します。

  • サーバー側は実行条件を減らし、UI側はTypeScriptの型安全性を活かす。
  • モジュール分割とサービス分割を分離し、デプロイ単位を増やしすぎない。
  • 移行対象は『動かない境界』に絞り、動いているUIまで無条件に置き換えない。
ランタイムの責務を分ける概念図
browser:   React + Vite + TypeScript
           |  generated client from .proto
           v
edge:      Cloudflare Pages Functions
           |  tenant resolution / API proxy
           v
backend:   Go modular monolith on Cloud Run
           |  auth / domain / validation / SQLC
           v
database:  PostgreSQL on Cloud SQL

テナント解決はエッジ、最終的な安全性はバックエンドとDBで担保します

複数ブランドや複数組織に同じアプリケーションを提供する場合、リクエストがどのテナントのものかを最初に決める必要があります。目標構成では、Cloudflare Pages FunctionsがHostを読み、ドメインとテナントIDの対応表をCloudflare KVから取得します。その後、APIリクエストへテナント情報を付与し、静的アセットとAPIの入口をエッジで分けます。

ただし、エッジが付けたヘッダーを信じるだけでは不十分です。Cloud Run側で許可された経路から来た情報かを検証し、リクエストコンテキストへテナントを注入します。さらに、SQLCが生成したクエリの引数にテナントIDを必須で渡し、すべてのデータ取得に分離条件を含めます。エッジ、アプリケーション、DBの三つの層で同じ境界を確認する設計です。

Cloudflare KVは高速参照のための投影であり、正しいブランド情報の正典ではありません。DBをマスターにし、更新イベントまたは同期処理でKVを更新します。KVの更新が遅れた場合に、未知のドメインを既定テナントへ誤って流さないことも、移行時に決めておく必要があります。

  • 高速化のためのKVと、正しさの正典であるDBを同じ役割にしない。
  • エッジの解決結果をバックエンドで再検証し、未知の値は安全側で拒否する。
  • テナントIDをDBクエリの必須引数にし、画面側の絞り込みだけに依存しない。
テナント境界の検証順序
request
  -> edge: Host -> tenant_id (KV lookup)
  -> backend: verify edge metadata
  -> context: attach tenant_id
  -> query: WHERE tenant_id = $1
  -> response: never cross tenant boundary

画面の増加を、サービスの増加に直結させないSingle SPAへ移します

フロントエンドを複数の独立アプリとして配信すると、個別デプロイの自由度が得られる一方、共通シェル、依存ライブラリ、認証、ブランド設定、リリース順序をそろえる負担が増えます。画面が増えるたびにサービス、コンテナ、リモートエントリ、監視対象が増える構成では、TypeScriptが動くかどうか以前に、全体を同じ状態へそろえることが難しくなります。

そこで、React Routerを使ったSingle SPAへ統合し、ルート単位の遅延読み込みとViteのCode Splittingで初期ロードを抑えます。ソースは一つ、ビルド成果物も原則一つにしながら、利用しないドメインのコードは最初からブラウザへ送らない設計です。Module Federationのような実行時のリモート依存を減らすことで、画面の表示に必要なバージョンの組み合わせを少なくできます。

この方式は、独立デプロイが必要なチームには向きません。ドメインごとに完全なリリースライフサイクルを持たせる必要がある場合は、複数アプリを維持した方がよいこともあります。今回の判断では、共通認証・ブランド注入・API契約を同時に更新する頻度が高く、デプロイの一貫性を優先しました。

  • 共有シェルと契約を一つのビルドへ集約し、実行時の依存組み合わせを減らす。
  • React.lazyとmanualChunksで、統合による初期ロード増加を抑える。
  • 独立デプロイが本当に必要な境界かを、チーム運用とリリース頻度から判断する。

APIとDBは、手書きの重複を減らして生成経路を一本化します

APIの入力・出力をGoとTypeScriptで別々に定義すると、片方だけ更新された時に実行時エラーが残ります。移行後は、`.proto`をAPI契約の正典にし、buf generateでGoのサーバー型、TypeScriptのクライアント型、React Queryのフックを生成します。手書きするのは契約とドメインの実装に限定し、境界の変換コードを減らします。

DBでは、Atlas HCLでスキーマを管理し、SQLCでSQLからGoの型付きアクセスコードを生成します。ORMにすべての問い合わせを任せるのではなく、SQLを読める状態に保ち、テナント分離条件、インデックス、トランザクション境界をレビュー対象にします。生成は便利ですが、生成物を正しさの根拠にしてはいけません。入力となるスキーマ、SQL、Protobufのレビューが先です。

この構成では、APIとDBの変更順序も重要です。新しいフィールドを先に追加し、旧版が読める期間を作り、データを埋め、利用状況を確認してから不要な項目を削除します。アプリケーションのロールバックとDBのロールバックは同じ操作ではないため、破壊的な変更を一つのリリースへ混ぜません。

  • API契約を一箇所に置き、GoとTypeScriptの型を同じ入力から生成する。
  • SQLを隠しすぎず、テナント条件とトランザクションをレビュー可能にする。
  • 追加・移行・削除を分離し、アプリケーションの切り戻しとDB変更を両立させる。
新規API追加の生成経路
1. write API contract      -> proto/service.proto
2. generate clients         -> buf generate
3. write domain behavior    -> Go handler/service
4. write SQL                 -> queries/*.sql
5. generate DB access        -> sqlc generate
6. verify compatibility     -> proto breaking + sqlc verify

運用の設計は、言語の置き換えと同じくらい重要です

Goへ移行しても、ビルドや配備が手作業のままなら、動作しない問題は別の場所へ移るだけです。BazelでGoとSPAの依存関係を追跡し、remote cacheを使って再ビルドを減らします。Daggerで生成、テスト、イメージ作成、配備の流れをコード化し、ローカルとCIで同じ検証手順を再現できるようにします。Nix FlakesはGo、Node.js、Bazel、bufなどの版を固定するために使います。

配備先は、静的ファイルをCloudflare Pages、エッジ処理をPages Functions、APIをCloud Run、データをCloud SQLへ分けます。Cloud Runは初期設定で0スケールを選べるため、利用量が少ない時の常駐コストを抑えられます。ただし、コールドスタート、DB接続数、外部APIのタイムアウトは実測し、必要になった時だけ設定を変えます。

監視は最初からすべての情報を集めません。Goのpanic、ハンドラーエラー、DBタイムアウト、Reactの描画エラー、決済や外部連携の検証失敗をSentryへ送ります。Performance MonitoringやSession Replayを無条件に有効化せず、エラーの発生箇所とリリースを結びつけるところから始めます。

  • ビルド・生成・テスト・配備の手順をコード化し、実行環境の差を減らす。
  • 0スケールはコスト上の選択であり、遅延やDB接続数を測らなくてよい理由にはしない。
  • 監視対象を絞り、失敗とリリースを結びつけられる証跡を先に作る。

移植は、実行基盤・契約・機能・配備の順に小さく進めます

全面移行を一度の切り替えにすると、TypeScriptの実行不良、認証、データ分離、UIの差分、配備設定が同時に失敗します。先に開発環境とビルドの再現性を整え、次にGo Core APIの骨格、DBスキーマ、Protobuf契約を作ります。その後、認証とテナント境界を実装し、ドメイン機能を一つずつ移植します。

フロントはSingle SPAの入口とブランド注入を作り、生成されたAPIクライアントで一つの画面から接続します。CloudflareとCloud Runの配備は、静的アセット、API、DB変更を同じリリースに詰め込まず、段階ごとにreadiness、主要なスモークテスト、テナント間のデータ分離を確認します。

移行中は、旧版をいつまで残すか、どのデータを二重に書くか、切り替え失敗時にどこへ戻すかを記録します。全面Go化が完了していない段階で、旧資産を先に削除しないことも重要です。移植の完了条件は『新しいコードがコンパイルできる』ではなく、機能等価性、契約互換性、監視、復旧手順まで確認できることです。

  • 実行環境の再現性を、機能移植より先に確認する。
  • 認証・テナント分離・金銭やデータに関わる処理は、独立した検証軸として扱う。
  • 旧版の削除条件を先に決め、移行中の切り戻し可能性を残す。
段階的な移行順序
phase 0  reproducible toolchain and build
phase 1  Go core / schema / proto contracts
phase 2  auth / tenant resolution / authorization
phase 3  domain behavior and Single SPA routes
phase 4  Cloudflare / Cloud Run / database rollout
phase 5  equivalence tests / observability / rollback proof
phase 6  retire old runtime only after evidence is complete

Goへ移せば解決する問題と、解決しない問題を分けます

Goは単一バイナリにしやすく、サーバーの実行条件をそろえる助けになります。しかし、テナントIDを付け忘れたSQL、認証Cookieの扱い、外部APIの再試行、DBマイグレーションの順序は、言語を変えても安全にはなりません。移行後も、契約テスト、権限テスト、データ分離テスト、障害時の復旧確認が必要です。

また、Single SPAはリリースの一貫性を高める一方、フロントの変更が同じ成果物に集まりやすくなります。ドメインごとの所有者、影響範囲、Code Splitting、ルート単位のテストを整えないと、今度は一つの巨大なフロントが新しい境界になります。集約する対象と、独立させる対象を定期的に見直します。

今回の設計は、TypeScriptを使い続けるフロントエンドと、Goへ移すサーバー・制御系の責務を分ける前提です。TypeScriptが動かなくなったという事実から、すべてを別の言語へ置き換えるのではなく、実行条件の揺れが大きい部分だけを移す。この境界が、移行コストと将来の運用リスクを同時に抑える判断になります。

  • ランタイムの安定化と、認証・権限・データ整合性の安全性を別に検証する。
  • Single SPAの集約効果と、巨大化するリスクをCode Splittingと所有境界で管理する。
  • 言語移行の完了条件を、コンパイルではなく等価性・観測・復旧の証跡で定義する。

移行の判断軸は、言語ではなく『同じ条件で動かせるか』です

TypeScriptで動かなくなった時、最初に考えるべきなのは、TypeScriptを捨てることではありません。どの実行境界で、どの依存関係が、どの配備条件とずれているのかを特定することです。今回の設計では、ブラウザのTypeScriptを残し、サーバー側をGoの単一バイナリへ集約し、Cloudflareのエッジ、Single SPA、Protobuf、SQLCで境界を整理します。

移行の成否は、設計書の完成や新言語のビルド成功だけでは決まりません。変更したコードを同じツールチェーンで生成・検証できること、テナント境界を各層で守れること、失敗した時に旧版へ戻せることを一つずつ確認します。動く状態を作り直すとは、コードを移すことではなく、動作条件と検証可能性を移すことです。

Technical documents

技術文書を増やしていきます。

AI、クラウド、業務アプリ開発、要件定義、運用設計に関する考え方を、今後も文書として整理します。

技術文書一覧へ