ハンズオン

アーキテクチャとエラー契約

Sushi SaaS が強制するレイヤー、ブラウザのデータフロー、認可境界、情報を漏らさない多言語エラー方式を説明します。

starter のコミット 7d452a6 に同期しています。

サーバーは一方向に流れる

src/app/**       ルートとページ:HTTP の入出力

src/services/**  業務ルール、調整、不変条件

src/models/**    型付き永続化;db() を呼べる唯一の層

src/db/**        スキーマ、マイグレーション、接続

ルートは認証・検証・HTTP 変換だけを担当し、書き込みは必ず service を通ります。Model はクエリ、テナント条件、トランザクションを所有します。tests/unit/architecture.test.ts が境界違反を検出します。

src/features/ は作りません。各ドメインは model、service、config、component の水平レイヤーに配置します。

ブラウザのデータフロー

Server Component → service を直接呼ぶ
Client Component → src/api/** → 共通 API client → /api/**

Server Component は自分の API を fetch しません。Client Component は fetch を直接使わず、src/api/ のドメインラッパーを使います。

認可の二つの問い

組織ロールとサブスクリプションプランは別です。

const ctx = await getOrgContext(request);
if (!ctx || !can(ctx, "file:delete", file)) return respForbidden();
await requireEntitlement(ctx.orgUuid, "storage.upload");

can() はロールが操作を許すか、entitlement は組織の有効プランが機能や上限を含むかを判断します。

情報を漏らさないエラー契約

サーバーは安定したカタログコードを持つ AppError を投げます。ルート境界の respError が内部詳細をログに残し、安全な翻訳文だけを返します。

UI は error_code で分岐し、resolveErrorMessage または resolveAuthError で文言を解決します。error.message は表示しません。翻訳は src/lib/errors/i18n/locales/ にあり、新しいコードは五言語すべてに追加します。

ドメイン追加の手順

  1. src/config/ に定数を置く。
  2. src/models/ に型付き CRUD を追加する。
  3. 不変条件、認可、冪等性、副作用を src/services/ に置く。
  4. ルートを薄く保ち、エラーカタログを使う。
  5. 適切な階層のテストを追加する。
  6. pnpm lintpnpm test:runpnpm build を実行する。

詳細な契約は starter の docs/errors.mddocs/frontend.mdAGENTS.md にあります。

アーキテクチャとエラー契約 · Sushi SaaS