Geb's Moon Logo

Geb's Lab

Notes are rough memos for capturing ideas as they come. They may be substantially revised or removed without notice. Since the ideas are usually turned into prose with AI assistance, they may contain inaccuracies.

サーバー・クライアントを一体化する設計はAI駆動開発を遅らせる?

7/6/2026

背景

近年のWebフレームワークは、サーバーとクライアントのコードを近づける方向に進んでいる。 React RouterRemixNext.jsReact RouterRemixloader/actionrouteNext.js App RouterServer ComponentsServer ActionsUI

この流れは、UI実装における見通しの悪さを解決するためには合理的である。 SPA + APIAPIDTO

一方で、AI時代には、サーバーとクライアントを近づけすぎることに新しいリスクがある。 AIDB

そのため、AI時代には「コードを近づけること」と「変更ガバナンスを分けること」を両立する必要がある。 AICIUI便

課題・懸念

最大の課題は、リスクレベルの異なるコードが同じ変更単位に混ざることである。 AIAI2PR

特に危険なのは、Server Actionsやroute actionを本物の業務バックエンドとして扱うことである。 Next.jsServer ActionsReact Router/Remixaction便DB便

一方で、昔ながらのSPA + APIサーバー分離に戻すだけでは、コードが散って見通しが悪くなる。 APIAPIDTOAI

さらに、domain層のロジックをサーバーとクライアントで二重実装したくないという問題もある。 UI2TypeScript

つまり、単純な「フルスタック統合」でも、単純な「API/Front横割り分離」でも不十分である。 API/Front

解決策の概要

方針は、コアサーバーとクライアントを分離しつつ、コードは機能ごとに凝集させることである。 DBBFF/SSRUIUX api/frontend/ の横割りを基本にするのではなく、features/todofeatures/project のように機能単位で切る。

構成としては、lite版は Core API + SPA、UX重視版は Core API + BFF/SSR + UI とする。 liteReact RouterSPA使Core APIAIUXReact Routerloader/actionBFF/SSR使Core APISSRCore API

BFF/SSR層は、Core APIと統合されたバックエンドではなく、フロントエンド領域のサーバー実行レイヤーとして扱う。 BFF/SSRSSRCore APIViewModelDB

機能単位の凝集は、vertical slice + runtime entrypoints で実現する。 domainAPI contractCore APIBFFUIimportAPI rootAPI entrypointimportWeb rootUI/BFF entrypointimportruntimeentrypoint

解決策の詳細

domain層は、共有可能な domain-kernel と、サーバー専用の domain-server に分ける。 domain-kernel には、ブラウザでも安全に動かせる純粋なロジックを置く。たとえば、値オブジェクト、状態遷移の候補計算、入力検証、金額計算、日付範囲計算、表示上の操作可能性判定などである。domain-server には、DB状態、認可、外部副作用、監査、課金、保存処理に関わる高リスクなロジックを置く。これにより、二重実装を避けながら、サーバーが最終責任を持つ構造を保てる。

クライアントで共有domainを使う場合、それは最終判断ではなくUXのための事前判断として扱う。 Core API

API契約は、domain objectではなく公開DTOとして定義する。 Core API使domain objectAPI request/responseDTODTOZod schemaOpenAPI contract に置く。API側もフロント側もこのcontractを参照するが、domainの内部実装には依存しない。

ディレクトリ構成は、機能単位を基本とする。

apps/
  api/
    src/main.ts              # Core APIのcomposition root
 
  web/
    app/root.tsx
    app/routes.ts            # React Routerのcomposition root
 
features/
  todo/
    domain-kernel/
      index.ts
      todo.ts
      todo-status.ts
      todo-validation.ts
 
    domain-server/
      index.ts
      todo-policy.ts
      todo-invariants.ts
 
    contract/
      index.ts
      todo.dto.ts
      todo.schema.ts
      todo.openapi.ts
 
    api/
      index.ts
      todo.routes.ts         # Hono route
      todo.handlers.ts
      todo.service.ts
      todo.repository.ts
 
    bff/
      index.ts
      todo.loaders.ts        # React Router SSR/BFF用
      todo.actions.ts
 
    ui/
      index.ts
      TodoListPage.tsx
      TodoEditor.tsx
      useTodoForm.ts

この構成では、rootは各機能のentrypointを統合するだけのcomposition rootになる。 apps/api は、各featureの api entrypointをimportしてCore APIのルーティングを構成する。apps/web は、各featureの uibff entrypointをimportしてReact Routerのルーティングを構成する。root側には業務ロジックを置かない。rootはあくまで、アプリケーション全体を組み立てる場所である。

SPA版とBFF/SSR版は、同じ機能構造のまま切り替えられる。 SPAui が生成API clientを使ってCore APIを直接呼ぶ。BFF/SSR版では、bff のloader/actionが生成API clientを使ってCore APIを呼び、ui はloader dataを受け取る。どちらの場合も、Core APIの内部実装には依存しない。React Routerを使うことで、SPAからBFF/SSRへの移行、またはその逆を比較的行いやすくなる。

技術スタック

フロントエンドおよびBFF/SSR層には、React Routerを使う。 React RouterSPAloader/actionBFF/SSR使lite Core API + SPA と、UX重視版の Core API + BFF/SSR を同じ設計思想で扱いやすい。画面単位のデータ取得、フォーム送信、リダイレクト、SSRを段階的に導入できる点が、この構成と相性がよい。

Core APIには、Honoを第一候補として使う。 HonoTypeScriptWebAPICore APIHono使HTTP adapterHono handlerapplication servicehandlerapplication/domain

API契約には、Zod + OpenAPIを使う。 request/response schemaZodOpenAPIBFF/SSROpenAPIAPI client使Core APICore APITypeScript便使

DBアクセスには、TypeScript ORMまたはクエリビルダを使う。 DrizzleDB api または infrastructure 相当のserver-only領域に閉じ込める。uibff からrepositoryをimportしてはいけない。BFF/SSR層はサーバーで動くが、DBに直接触らず、Core APIを通してデータにアクセスする。

モノレポ構成では、package managerのworkspaceとexportsを使って境界を作る。 featurepackageentrypoint exports で制限する。外部からは ./domain-kernel./contract./api./bff./ui のような明示的entrypointだけをimportできるようにする。深いパスへのimportは禁止する。これにより、物理的には近くに置きつつ、依存境界を保てる。

{
  "name": "@app/todo",
  "exports": {
    "./domain-kernel": "./src/domain-kernel/index.ts",
    "./contract": "./src/contract/index.ts",
    "./api": "./src/api/index.ts",
    "./bff": "./src/bff/index.ts",
    "./ui": "./src/ui/index.ts"
  }
}

実装ルール

最重要ルールは、Core APIを迂回しないことである。 BFF/SSRDBserver-only domainimportCore APIBFF/SSRCore APIDBCore API

import可能な依存方向を明示的に制限する。

allowed:
  features/*/ui              -> features/*/domain-kernel
  features/*/ui              -> features/*/contract
  features/*/ui              -> packages/api-client
 
  features/*/bff             -> features/*/domain-kernel
  features/*/bff             -> features/*/contract
  features/*/bff             -> packages/api-client
 
  features/*/api             -> features/*/contract
  features/*/api             -> features/*/domain-kernel
  features/*/api             -> features/*/domain-server
 
forbidden:
  features/*/ui              -> features/*/api
  features/*/ui              -> features/*/domain-server
  features/*/ui              -> features/*/repository
 
  features/*/bff             -> features/*/api
  features/*/bff             -> features/*/domain-server
  features/*/bff             -> features/*/repository
 
  features/*/api             -> features/*/ui
  features/*/api             -> features/*/bff

domain-kernel は、ブラウザで安全に動く純粋ロジックだけに限定する。 domain-kernel に置いてよいものは、副作用がなく、手元のデータだけで判断でき、ブラウザに出しても問題ないルールである。たとえば、値オブジェクト、formatしない純粋な計算、入力検証、状態遷移の候補計算などである。置いてはいけないものは、DB状態に依存する判断、認可の最終判断、秘密情報を使う処理、外部サービスとの連携、監査や課金に関わるルールである。

contract は、外部利用者に約束するAPI仕様だけを持つ。 contract には、request DTO、response DTO、Zod schema、OpenAPI定義、エラーコード、ページング仕様などを置く。domain objectをそのまま公開しない。API側はdomain objectからDTOへ変換して返す。フロントエンド側はDTOを受け取り、必要に応じてViewModelへ変換する。これにより、domain内部の変更とフロントエンドの変更を切り離す。

Hono handlerには業務ロジックを書かない。 handlerHTTP adapterapplication serviceapplication servicedomain-serverrepositoryhandlerapplicationHTTP

React Routerのloader/actionは、BFFとして扱う場合でもCore APIの薄い利用者にする。 loader/actionCore APIDBrepositoryserver-only domainimport便

AIに任せる範囲とレビュー基準をruntimeごとに変える。 uibff は比較的低リスク領域として扱い、AIによる実装支援を積極的に使う。レビューは画面差分、型、主要導線、E2E、アクセシビリティ、API契約違反の有無を中心にする。apidomain-serverrepository は高リスク領域として扱い、人間レビュー、設計確認、認可確認、DB migration確認、契約テストを厚くする。domain-kernel は共有範囲が広いため、中リスク領域として扱い、変更影響に注意する。

CIでは、import境界、型、契約、テストを自動で検査する。 lintboundary checkerimportTypeScript project referencesworkspaceOpenAPI schemaCore APIapplication/domainUI/BFFE2E

残る課題・懸念

最大の残課題は、domain-kernel の境界設計である。 domain-kernel に多くのロジックを入れたくなる。しかし、ここが膨らみすぎると、クライアントに出してはいけない判断が漏れる可能性がある。特に、認可、契約状態、在庫、課金、外部サービス状態、DB上の一貫性に依存する判断は危険である。domain-kernel は便利な共有置き場ではなく、ブラウザに出しても安全な純粋ルールの置き場として厳密に扱う必要がある。

BFF/SSR層がCore APIを侵食する危険は残る。 BFF/SSRDBserver-only moduleimportlintexportspackageCIBFFCore API

機能単位凝集は、横断関心事の置き場所を難しくする。 feature flagfeaturefeaturepolicy

OpenAPI契約を介することで、型共有の即時性は少し落ちる。 tRPCHono RPCOpenAPI + generated client Core APIDX

SPA版とBFF/SSR版を完全に同一コードで切り替えることは難しい可能性がある。 React RouterfeatureSPABFF/SSRentrypoint

AI向けの境界ルールは、ドキュメントだけでなく機械的制約に落とし込む必要がある。 AI使moduleimportAIREADMEpackage exportslintCICODEOWNERSPRreview policyAI

まとめ

この設計の狙いは、フルスタック統合とサーバー・クライアント分離の良いところを両立することである。 AIAICore APIFrontend/BFF

最終的な形は、Core API分離 + vertical slice + runtime entrypoints である。 Core APIFrontend/BFFdomain domain-kernel とサーバー専用の domain-server に分ける。API契約は contract に置き、OpenAPI + generated clientで接続する。各featureの中にAPI、BFF、UIを近くに置きながら、importルールとCIで境界を守る。

この構成は、AI時代のWebアプリケーションにおける「近接と分離の両立」を目指す設計である。 AICore APIUXAI