APIとDBの責任境界
UIを信用せず、それでも使いやすいAPIを作るには、入力検証・認証・認可・業務不変条件・error公開をどこで担当するか決める必要があります。
境界、transaction、並行制御、脅威、テスト、障害対応をFailure Firstで学ぶ。
先に学ぶ内容 — Prerequisite: 基礎 / 銀行システム学ぶこと — LEARNING GOALS
このページで学ぶこと
- UI・API・DBの責任分担
- 認証と認可の実行位置
- 安全なerror contract
体験30 · API / DB BOUNDARIES(責任を重ねる)
画面のボタンを無効化しておけば、APIへの不正な直接呼び出しも防げるか確かめよう
まずは予想をひと言。本文を読んだ後、教材内の参照元で実際のAPIとエラー変換を確認します。
- 01まず予想する — PREDICT
まずここだけ:思ったことをひと言書く
画面のボタンを無効化(disabled)しておけば、そのAPIへの不正な直接呼び出しも防げると思いますか?
正解でなくて大丈夫です。「〜だと思う」から始めてみましょう。
ヒント1を見る
UIの制御と、サーバー側の確認は同じ場所で行われているでしょうか?
ヒント2を見る
APIを直接呼び出すツールがあったら、画面の制限は意味を持つでしょうか?
もっと詳しく:この問いで使う専門用語
- Trust boundaryは、入力やidentityをそのまま信用できなくなる境目です。
- Defense in depthは、独立した複数の防御を重ねる考え方です。
- Error contractは、callerが安定して扱えるerror codeと意味の約束です。
日常語で理解する — PLAIN LANGUAGE
まず、ここだけつかむ
UIの非表示やdisabledは操作支援であり、認可ではありません。
APIは公開contract、DBはどの呼出経路でも破れないownership・状態・金額不変条件を担当します。
内部診断、安定error code、利用者messageを分離します。
順番に理解する — DIFFICULT PARTS, STEP BY STEP
難しいポイントを、順番にほどく
専門用語を覚えることより、「なぜ必要か」「このシステムのどこで使うか」を理解することが目標です。
Trust boundary
入力やidentityをそのまま信用できなくなる境目です。
境界を跨ぐたびに検証責任を明確にします。
browser→API→PostgREST→SECURITY DEFINERで再確認します。
Defense in depth
独立した複数の防御を重ねる考え方です。
1層の実装漏れが直ちに資金事故にならないようにします。
validation、Auth、RLS、RPC認可、constraint、auditを重ねます。
Error contract
callerが安定して扱えるerror codeと意味の約束です。
DB messageは変化しやすく内部情報も含むためです。
SQLSTATEを安全なAPI errorへmappingします。
発展 — ADVANCEDもっと詳しく確認する最初の学習では読まなくても問題ありません。仕組みをもっと深く理解したい場合に開いてください。
同じ検査を重ねる理由
| 層 | 主な責任 | 任せてはいけないこと |
|---|---|---|
| UI | 入力支援、確認、二重click抑止 | 最終的な認可・金額整合性 |
| API | token検証、schema検証、公開contract、rate制御 | client申告のownerやadmin roleを信用すること |
| Service | use caseの組立て、error mapping | DB transactionを複数callへ分断すること |
| DB RPC | caller再確認、状態・lock・不変条件・atomic write | 画面が正しく呼ぶという前提 |
| Constraint / privilege | どの経路でも破れない最後の防御 | 業務上の説明や利用者向けmessage |
認証情報の流れ
- 01browserがaccess tokenを送る
- 02serverが署名済みtokenから利用者を確認
- 03APIがclient入力のuser idを捨てる
- 04DBがauth.uid()からcallerを再解決
- 05RLSまたはRPCが対象ownershipとroleを確認
- 06許可されたrecordだけを返す
errorを3層へ分ける
| 層 | 例 | 公開範囲 |
|---|---|---|
| 内部診断 | constraint名、SQL message、stack、request context | 権限を限定したlogのみ |
| 安定code | INSUFFICIENT_FUNDS、INVALID_STATE | API contractとprogram分岐 |
| 利用者message | 残高が不足しています | 画面。安全で行動可能な表現 |
設計演習:API直呼びを想定する
- 別人のbank_account_idへ振込requestを送る
- admin画面を経由せずfreeze RPCを呼ぶ
- 同じidempotency keyで金額を変える
- enum外の状態・負数・巨大値・余分なfieldを送る
- 内部errorを意図的に起こしresponseを確認する
実際のMinibankでは
Next.js routeはAuth clientからuserを確認し、serviceを通じて公開RPCを呼びます。SECURITY DEFINER RPCは強権限だからこそ、`auth.uid()`、owner、role、status、固定search_path、EXECUTE権限を関数ごとに読みます。
PostgreSQLの生messageはclient contractにせず、安定codeを`mapRpcError()`で安全な日本語へ変換します。
体験学習の続き · LEARNING LOOP
ここまで読めましたか?続けて確認してみましょう
Trust boundaryとDefense in depthの考え方を読んだところで、教材内の参照元で実際のAPIとエラー変換を確認し、自分の言葉で説明してみます。
- 02仕組みを確かめる — CONCEPT CHECK
画面操作なしで、仕組みを確かめる
この仕組みは、Mini Bankの画面には現れません。次のEvidenceとCode Trail、教材本文だけで理解を進めます。
- 03証拠を確認する — EVIDENCE
何を見たか、1つずつ確かめる
Evidence(判断の根拠となる記録)を確認したらチェックします。各項目の「何を見るか」を手がかりにしてください。
画面を開けない項目は、Source Trail(教材の参照元)と教材の説明で確認できます。失敗を実際に起こす必要はありません。
- 04コードの処理経路を追う — CODE TRAIL
コードを書く必要はありません
ここでは「説明されている仕組みが、実際のMini Bankのどこに実装されているか」を確認します。まずは日本語の役割説明を読み、入力した依頼がどこへ渡るかをつかみましょう。コードの全文を読む必要はありません。
まずHTTP境界がcallerをどう再確認するかを見て、次にDBエラーがどう安全な形式へ変換されるかを見ます。
教材の参照元を確認する — Source Trail ↓HTTP境界とrequest validation。callerの再解決をどこで行うかを探します。
src/app/api/v1/DB errorから公開errorへの変換。
src/lib/errors/rpcErrors.ts - 05自分の言葉で説明する — EXPLAIN
LEVEL 1 — やさしい説明(まずここだけ)
なぜUIで操作を制限するだけでなく、API・DBのそれぞれでも同じ確認を繰り返す必要があるのでしょうか? 短い2文を目安に説明してください。
日常語だけで完了できます。専門用語の数や正誤を自動採点するものではありません。
ヒント1を見る
「Trust boundary」と「Defense in depth」という2つの言葉を使って書いてみましょう。
ヒント2を見る
UI・API・DBのどこが最後の防御かを考えてみましょう。
もっと詳しく:LEVEL 2 — 専門用語を使った説明(任意)
Trust boundary、Defense in depth、Error contractの3つを使って、UI・API・DBの責任分担を説明してみましょう。
挑戦する場合は、上の説明欄に追記できます。追加しなくても完了できます。
説明の確認ポイント — Checkpoint- UIの制限とサーバー側の認可は別だと説明できる
- 内部診断情報と利用者向けメッセージが分離されていると説明できる
理解を確かめる — CHECK YOUR UNDERSTANDING
3問で理解チェック
答えを頭の中で考えてから、各項目を開いて確認してください。
Q1APIがowner確認すればDBでは不要ですか?
不要ではありません。別APIやRPC直呼び、将来のfilter漏れに備えDB境界でも確認します。
Q2service_roleで全tableを読み、APIで絞るのは安全ですか?
通常利用経路では危険です。filter漏れが全件漏洩になるため、最小権限とDB側の範囲制御を使います。
Q3DBのmessage_textをそのまま返せますか?
返しません。内部詳細を分離し、安定codeを安全な利用者messageへ変換します。
完全に暗記する必要はありません。大切な考え方を説明できそうなら押してください。
教材の参照元 — SOURCE TRAIL
根拠と関連教材
CURRENT IMPLEMENTATIONは確認したrepository内の実装です。HISTORICAL IMPLEMENTATIONは導入・修正の履歴で、後続migrationによる再定義があり得ます。RELATED LESSONは教材への案内で、実装の一次Sourceではありません。ASSUMPTIONは仮定、UNKNOWNは未確認を表します。
確認基点: d02eae8914f39cbf06bfe422dc39c71886f82b3e / 2026-09-11。稼働環境への適用確認: UNKNOWN。パス実在の自動検査と、記述した技術的事実の確認は別です。
- 現在の実装 — CURRENT IMPLEMENTATION
src/app/api/v1/HTTP境界とrequest validation - 現在の実装 — CURRENT IMPLEMENTATION
src/lib/errors/rpcErrors.tsDB errorから公開errorへの変換 - 過去の実装 — HISTORICAL IMPLEMENTATION
supabase/migrations/RLS・GRANT・SECURITY DEFINER RPC