APIデザイン・パターン

APIデザイン・パターン

に公開

本書の目的

  • Web APIを構築する際に用いられる、安全かつ柔軟で、再利用可能なデザイン(設計)パターンを提供すること

PART 1 はじめに

第1章 API入門

APIがなぜ重要かを掘り下げる章です。

用語

  • インターフェース: 2つのシステムがどうやり取りするかを定めた「契約」
  • API: コンピュータシステムとやりとりをする方法を定義するもの。ほとんどのシステムは孤立して動いているわけではないので、APIは至る所に存在する
  • Web API: ネットワーク上で公開され、さまざまな人がリモートで使用することを目的として作られた特殊なAPI

リソース指向という発想

本書全体の前提が「リソース指向API」です。処理(動詞)を無数に並べるのではなく、限られた「リソース(名詞)」に対して、標準的な「メソッド」群を定義することで複雑さを抑えます。リソースを1つ覚えれば、それに対してできる操作(作成・取得・一覧・更新・削除)も自動的に分かる、という予測可能性が生まれます。

良いAPIの条件

何が「良い」かは完全には自明ではありませんが、一般に良いAPIは運用(実行)可能・表現力がある・シンプル・予測可能であるとされます。以降の全パターンは、この4つの軸で評価されます。

第2章 APIデザインパターン入門

実際にAPIを作るときにどう「型」を当てはめるかを扱う章です。

デザインパターンとは

ソフトウェア設計が「コードの構造・レイアウト」だとすると、デザインパターンは「特定の設計を、似た問題に少しずつ調整しながら何度も適用できるときに生まれる型」です。APIを設計・構造化するための設計図のようなものです。

なぜ重要か

Web APIは一度公開すると非常に硬直的で、変更が容易ではありません。だからこそ、後からの大規模な作り直しを最小化するパターンが効いてきます。


PART 2 設計の原則

第3章 名前付け

名前はAPIのあらゆる場所に潜み、意図したよりずっと長く生き残ります。だから良い名前を選ぶことが重要になります。

良い名前の条件

良い名前は、良いAPIと同じくシンプル・表現力がある・予測可能であるべきです。

具体的な指針

  • 言語・文法・構文の選択は、「何を選ぶか」より「一貫して使うこと」が正解である。
  • 名前に前置詞が現れるのは、しばしばより深い設計問題を示す“不吉な臭い”である。
  • 名前が置かれるコンテキストは、情報を与える一方で誤解のもとにもなる。文脈に注意して名前を選ぶ。
  • 単位など名前に書ききれない情報は、専用のデータ型で表現する(例:timeoutMillis という名前に頼るより、ミリ秒を表す型を使う)。

練習問題

Q1. 定期的なスケジュール(「このイベントは月に1回発生します」)を管理するAPIを作っているとします。あるシニアエンジニアは「イベント間の秒数を保存すれば十分」と言い、別のエンジニアは「時間単位(秒・分・時間・日・週・月・年)ごとに異なるフィールドを提供すべき」と考えています。どちらの設計が、意図された機能の意味を正しく扱えてより良い選択でしょうか?

A1.「月に1回」のような周期は暦に依存し、月や年は長さが一定ではないため、単なる「秒数」では本来の意味を保てません。時間単位を区別できる専用の表現の方が利用者の意図を正しく表せ、表現力で優れます。「単位など名前に書ききれない情報は専用のデータ型で表す」という指針に沿い、後者がより良い選択です。

Q2. 容量はGB(10^9)で、スループットはGビット(2^30 bit)で測り、帯域制限を bandwidthLimitGib = 1 のように設定できるようにします。この違いは微妙すぎてAPI利用者を混乱させる可能性がありますか?その理由は?

A2. 混乱を招きます。同じ「G」でも10^9(ギガ=SI接頭辞)と2^30(ギビ=2進接頭辞)は別物で、しかも一方はバイト、もう一方はビットです。sizeGBとbandwidthLimitGibという名前の差だけで単位・基数の違いを背負わせると取り違えやすく、単位は名前に頼らず専用のデータ型で明示し、命名は一貫させるべきです。

第4章 リソースの範囲と階層

標準メソッドが多くのリソースへ一律に効くのは便利ですが、その裏返しとして「どんなリソースを定義するか」を慎重に選ぶ必要が高まります。

リソースレイアウト

リソースレイアウトとは、API内のリソースの配置と関係のことです。参照・多対多などの関係を、実体関係図(ER図)で表しながら設計します。

リソースにするか、データに埋めるか

ある概念を独立したリソースにするか、親に埋め込んだデータ(型) にとどめるかは、「その概念とのやり取りにアトミック性(不可分性) が必要か」で判断します。

# インライン化(Address型として埋め込む)
User { id, name, address: { street, city } }   # 住所を単独で操作しない

# リソース化(独立させる)
Address { id, street, ... }   # 単独で get/update/delete したい
User { id, addressId }        # 参照だけ持つ

アンチパターン

  • 全リソースを繋ぎたくなる衝動を抑え、重要な機能を提供する場合にだけ関係を持たせる。過度に深い階層は理解しづらく管理も難しいので避ける。
  • 単独でアトミックに扱う必要があればリソース化、そうでなければデータに埋める。

練習問題

Q1. URLブックマークをフォルダーで整理できるAPIを設計します。Bookmark と Folder という2つの別リソースにしますか?それとも単一リソース(Entity)にし、フォルダーかブックマークかを示す型をインラインで持たせますか?

A1. 多くの場合、BookmarkとFolderは別々のリソースにモデル化するのが素直です。両者は扱うデータと操作がともに異なり、それぞれ独立にget/update/deleteしたい(単独でアトミックに扱う)からです。単一リソース+型インライン(ポリモーフィズム、第16章)は「両者を1つの並びとしてまとめて扱うこと自体に意味がある」場合に限って選ぶべきで、単なる整理用途では過剰です。

Q2. 多対多・1対多・1対1の関係を持つ例を考え、適切な記号を使って実体関係図を描いてください。

A2. 例として、ユーザーとグループは多対多(User ⟷ Group、間にMembershipを挟む)、ユーザーと投稿は1対多(User 1 ─ ∞ Post)、ユーザーとプロフィールは1対1(User 1 ─ 1 Profile)です。ER図ではカラスの足記法で「多」を鳥の足、「1」を縦棒で示し、各端の基数を明示します。多対多は直接つながず関連リソースを挟むのが定石です。

第5章 データ型とデフォルト値

APIが受け取り・保持するデータ型は、設計時に常に意識すべきです。

null と「未定義/欠落」の区別

あらゆる値で、nullundefined(欠落)の両方を考える必要があります。両者は同じ意味のこともあれば違うこともあります。

型ごとの指針

  • ブール値は肯定形で命名(例:disableFeature ではなく enableFeature)。
  • 数値は単なる数字でなく意味を持たせる。桁あふれや浮動小数点の問題を避けるため、適切な表現がない言語では文字列でシリアライズする。
  • 文字列はUTF-8でエンコードし、識別子に使うなら正規化形式Cで正規化する。
  • 列挙型はなるべく避け、文字列+サーバー側検証にする(後から値を足しやすく、互換性を壊しにくい)。
  • リストはアトミックな集合として扱い、個々の要素の位置指定はしない。
  • リスト・マップは要素数を制限し、マップはキーと値のサイズも制限する(レスポンスの肥大化・DoSを防ぐ)。

練習問題

Q1. 日本語話者だけを対象とする企業が、文字列フィールドにUTF-8でなくUTF-16を使いたいと考えています。決める前に考慮すべきことは何でしょうか?

A1. UTF-16は日本語だけを見れば効率的でも、相互運用性で不利です。Web標準・多くのツール・JSONはUTF-8が事実上の標準で、UTF-16にすると他システムとの連携やバイト順(BOM/エンディアン)の扱いで問題が生じます。識別子や文字列は原則UTF-8でエンコードすべきで、目先の効率より長期の互換性を優先します。

Q2. ChatRoomが24時間より前のメッセージをアーカイブしています。この動作を無効にするフィールドにはどんな名前を付けるべきでしょうか?それは最良の設計でしょうか?

A2. ブール値は肯定形で名付ける原則から、disableArchivingではなくarchiveOldMessages(trueで有効)のように肯定形にします。ただしオン/オフだけのブールより、保持期間を数値(例messageRetention)で持たせる方が表現力に優れ、将来「48時間」などの要求にも応えられます。単なる無効化フラグは最良とは限りません。

Q3. 任意精度整数をサポートする言語(Python 3 など)で書く場合、数値を文字列としてJSONにシリアライズするのは良いことでしょうか?標準のJSON数値表現を使えるでしょうか?

A3. 書いている言語が任意精度でも、利用者側(JavaScriptなど)はIEEE754倍精度しか持たず、2^53を超える整数を失います。したがって安全のため、大きくなりうる数値は文字列としてシリアライズすべきです。サーバー言語が扱えることと、すべてのクライアントが扱えることは別問題なので、標準のJSON数値表現に頼り切るのは危険です。

Q4. チャットルームで使う言語を表すのに、サポート言語の列挙値を持てるでしょうか?無理ならどんなデータ型が最適でしょうか?

A4. 列挙型は避けるべきです。サポート言語は将来増減し、列挙値の追加・削除は既存クライアントの互換性を壊しやすいからです。代わりにBCP 47言語タグ(“ja”、“en-US” など)の文字列フィールドにし、検証はサーバー側で行うのが最適です。集合が開いている/進化するものは文字列+サーバー検証が定石です。

Q5. マップの値のサイズが著しく不均一な場合、キーと値に適切なサイズ制限を設けるにはどんな方法がありますか?

A5. マップにはキー数の上限に加え、キー長と値長それぞれの上限を設けます(例キー100文字・値500文字)。値の大きさが不均一なら、合計バイト数に上限を設けて「小さな値を多数」か「大きな値を少数」かを利用者へ委ねる方法もあります。いずれにせよ無制限とせず、レスポンス肥大化やDoSを防ぐ有界な設計とします。


PART 3 基礎編

第6章 リソース識別子

識別子はAPI内の特定のリソースを一意に指し示す値です。良い識別子と悪い識別子の差が、使いやすさを左右します。

良い識別子の条件

使いやすい・一意・不変・素早く生成できる・予測不可能・読みやすい・コピー/共有可能・情報量が多い、といった性質を満たすものが良い識別子です。

実装の指針

  • 利用者が触れる識別子はASCII文字列にする(URL・コピペ・読み上げに強い)。
  • できれば Crockford の Base32 を使う。これは人間向けに設計され、紛らわしい I(→1)・L(→1)・O(→0)・U を除き、入力時に自動で読み替える。
打ち間違い・見間違いに強い:
  利用者が O と書いても 0 に、I と書いても 1 に復元できる
  • チェックサム文字を末尾に付け、「打ち間違い」と「存在しない」を区別できるようにする。
正しい:  5H8KX2 + チェック文字7 = 5H8KX27
誤入力:  5H8KY27 → 5H8KY2 から再計算したチェック文字が7と一致しない
        → DBに問い合わせる前に「入力ミス」と判定できる

練習問題

Q1. 識別子のサイズに応じてより大きなチェックサムを用いる、新しいエンコード/デコード方式を設計しなさい。

A1. 識別子の長さ(ビット数)に比例してチェック桁を増やす設計にします。たとえばnバイトごとに1チェック文字を割り当て、全体に対する剰余(Crockford互換の基数のmod)でチェック値を計算します。識別子が長いほど誤り検出能力が上がり、長い識別子での打ち間違い見逃しを減らせます。デコード時はチェック文字を再計算して一致を確認します。

Q2. ランダムに選ばれた2バイト(16ビット)識別子を用いた場合の衝突の可能性を計算しなさい(ヒント:誕生日問題)。

A2. 空間は2^16 = 65,536個です。誕生日問題より、衝突確率が約50%になるのは概ね √(2^16) = 256個前後を生成したとき(厳密には約300個で50%)です。つまり数百個発行しただけで衝突が現実的になり、16ビットは識別子として小さすぎることが分かります。実用では十分大きな空間(128ビット級)を使うべきです。

Q3. 2バイト(16ビット)識別子を使う際に、単一のカウンタに依存しない衝突回避アルゴリズムを設計しなさい(ヒント:分散配置)。

A3. 16ビットを領域分割し、ノードごとに別々の範囲(プレフィックス)を割り当てる方法が使えます。たとえば上位ビットをノードID、下位ビットを各ノードのローカルカウンタにすれば、中央のカウンタを共有せずにノード間衝突を避けられます。あるいは各ノードが「自分の担当ブロック」を事前に予約して払い出す方式でも、単一カウンタなしに一意性を保てます。

第7章 標準メソッド

予測可能なAPIにするには、リソースに対してできる操作を固定し、一貫させることが有効です。

標準メソッド

get・list・create・update・deleteの固定セットを定義します。準標準としてreplaceもあります。重要なのは、すべてのcreateが同じように動くなど、同じ動作原則に従うことです。

冪等性

あるメソッドを繰り返し呼んでも、以後の結果が同じになる性質を冪等性と呼びます。すべてが冪等なわけではなく、直感に反して delete は冪等ではありません(2回目は「存在しない」状態になりうる)。副作用でシステムのどこかが変わるのは避けます。

トレードオフ

標準メソッドは振る舞いを狭く縛る代わりに、利用者が既存知識を流用できる「学びやすいAPI」という見返りを得ます。

練習問題

Q1. create と update メソッドを用意せず、replace メソッドだけにしてもよいでしょうか?

A1. 原理上はreplace(全体置換)で作成と更新のどちらも表現できますが、推奨されません。replaceは「リソースが無ければ作り、ある場合は丸ごと置き換える」になり、部分更新や、作成と更新で異なる検証・権限を分けられません。利用者の意図(新規と変更のどちらか)が曖昧になり予測可能性が下がるため、createとupdateを分けて提供するのが定石です。

Q2. ストレージが新規作成データの一貫性を保証できない場合、create のガイドラインを守ってデータを作成するにはどんな選択肢がありますか?

A2. 書き込み後すぐ読めない(結果整合性)なら、createのレスポンスでサーバーが返した値を信頼できる正とし、直後のgetでは「まだ見えない」可能性を受け入れます。選択肢としては、read-your-writes一貫性を提供する・作成完了をLRO(第10章)で表す・作成直後はレスポンスのボディを唯一の真実として扱う、などがあります。

Q3. update メソッドは、なぜ HTTP PUT ではなく HTTP PATCH を使うのでしょうか?

A3. PUTは本来「リソース全体の置換」を意味し、送らなかったフィールドは消える解釈になります。標準のupdateは部分更新(フィールドマスク、第8章)を扱うため、部分変更を表すPATCHが適切です。PUT相当の全体置換はreplaceに対応します。

Q4. ヒットカウンターを更新する get メソッドは冪等性を持ちますか?delete メソッドについてはどうでしょうか?

A4. 呼ぶたびにカウンターが増えるgetはべき等ではありません(毎回状態が変わる)。そもそもgetは副作用を持つべきではなく、これは設計上の不吉な兆候です。deleteもべき等ではありません。1回目は削除に成功しますが、2回目は「存在しない」状態になり、結果(や返るエラー)が変わりうるからです。

Q5. list メソッドで、結果の総数を含めたり、ソートをサポートしたりするのを避けるべき理由は何ですか?

A5. 総数は巨大コレクションで毎回数えると高コストで、ページングと相性も悪く、同時更新で不正確になります。任意ソートはインデックスの無いフィールドで重くなり、ページトークンの一貫性も保ちにくくします。どちらも性能・スケール・一貫性を損なうため、標準のlistでは基本的に避け、本当に必要なら限定的・明示的に提供します。

第8章 部分的な更新と取り出し

リソース全体を毎回送らせると、送り忘れによる消失や、他人の変更を巻き込む上書き(衝突)が起きます。

フィールドマスク

更新・取得の対象フィールドを示す「フィールドマスク」を使います。

UpdateUser { user: { phone: "..." }, field_mask: "phone" }
→ phone だけ更新、他は一切触らない(衝突しない)

GetUser { id, field_mask: "name,email" }
→ name と email だけ返す(巨大リソースの一部だけ取得)

細かいルール

  • ネストしたフィールドやマップのキーは指定できる(profile.bio, settings.theme)。
  • 配列の位置・インデックス指定はしない(同時編集で添字がずれるため)。
  • 既定の解釈は、取得では「省略時は全フィールド」、更新では「送られたフィールドの存在から暗黙的に推測」。
  • 無効なフィールドは「存在するが値がundefined」のように扱い、エラーで弾かない(互換性のため)。

練習問題

Q1. ChatRoom に1人でなく複数の管理者がいるとします。配列フィールドでこれらを格納する場合、どんな問題があり、どんな代替案がありますか?

A1. 管理者を単純な配列で持つと、部分更新できず(1人追加するのに配列全体を送る必要があり衝突しやすい)、要素の順序や重複の扱いも曖昧になります。代替として、管理者を関連付け(アソシエーションリソースやadd/removeカスタムメソッド/第14・15章)でモデル化すれば、個別に追加・削除でき、関係のメタデータ(役割など)も持てます。

Q2. 動的なデータ構造(マップ)から単一の属性を削除するにはどうすればよいでしょうか?

A2. フィールドマスクで対象キーを指定し、その値をnull(または欠落)として更新します。マスクにsettings.themeのように当該キーを含め、値を送らない/nullにすることで、そのキーだけを安全に削除でき、他のキーには触れません。

Q3. 部分取得を用いる場合、対象とするフィールドをどのように伝えればよいのでしょうか?

A3. 取得リクエストにフィールドマスク(例field_mask=name,emailやネストのprofile.bio)を付け、サーバーは指定フィールドだけを返します。これにより巨大リソースの一部だけを効率よく取得できます。

Q4. フィールドマスクにすべてのフィールドを列挙せず、全フィールドを取得(または更新)したい場合はどうすればよいでしょうか?

A4. 取得では「マスク省略=全フィールド」を既定にします。明示したいなら _(ワイルドカード)を全フィールドの意味として使う方法もあります。更新では「送られたフィールドの存在から暗黙にマスクを推測する」既定にし、全体更新を意図するなら _ を使います。

Q5. settings というマップのキー "hello.world" を更新するには、どんなフィールドマスクを使えばよいでしょうか?

A5. キー名自体にドットを含むため、そのままではsettings.hello.world(ネスト指定)と区別できません。キーを引用してエスケープし、settings.“hello.world” のように表記して「hello.worldというキー1つ」を指すようにします。マスクの構文でキーをクォートできるよう設計しておく必要があります。

第9章 カスタムメソッド

メール送信やその場翻訳のように、標準メソッドにうまく収まらない操作があります。無理にupdateに押し込むとインターフェースが複雑化し、利用者を混乱させます。

カスタムメソッド

標準に収まらない操作を、カスタムメソッドとして安全に公開します。

POST /missiles/1234:launch     ← コロンでリソースとアクションを区切る
POST /chatRooms/123/messages:archive

指針

  • HTTPは原則 POST(冪等かつ安全ならGETもありうる、PATCHではない)。
  • 副作用は標準メソッドでは禁止だが、カスタムメソッドでは許容。ただし控えめに使い、必ずドキュメント化する。
  • 複数リソースを対象にするなら、親ではなくコレクションを対象にする。
  • 計算特化のステートレスなカスタムメソッドは慎重に(後からステートフル化が難しい)。

練習問題

Q1. リソースを作成する際に何らかの副作用が必要な場合はどうすればよいでしょうか?これはカスタムメソッドにすべきでしょうか?その理由は?

A1. 標準のcreateは副作用を持つべきではないので、メール送信などの副作用が本質的に必要なら、それをカスタムメソッドとして切り出すのが適切です。ただし副作用は控えめにし、必ずドキュメント化します。単に作成のついでに軽い副作用が起きる程度なら新メソッドは不要で、利用者が驚く副作用ほどカスタムメソッドで明示すべきです。

Q2. どのような場合にカスタムメソッドはコレクションをターゲットにすべきでしょうか?親リソースはどうすればよいのでしょうか?

A2. 複数のリソースをまたいで作用する操作(一括処理など)は、特定の親ではなくコレクションを対象にします(例 /messages:export)。単一リソースに作用するならそのリソースを、親そのものの状態を変えるなら親を対象にします。「対象は作用範囲に一致させる」のが原則です。

Q3. ステートレスなカスタムメソッドだけに頼るのが危険なのはなぜでしょうか?

A3. 純粋な計算(例その場翻訳)をステートレスなカスタムメソッドで出すと手軽ですが、後から「結果を保存して参照したい」「履歴を残したい」というステートフルな要求が出たとき、リソース指向への作り直しが難しくなります。最初からリソースとして表せないか検討し、ステートレス特化は慎重に選ぶべきです。

第10章 ロングランオペレーション(LRO)

動画変換のように数分かかる処理を同期で待たせると、接続がタイムアウトし、進捗も分からず、途中キャンセルもできません。

LRO = 進行中を表す引換券

処理を即座に始め、結果ではなく「進行中の作業を表すOperation」をすぐ返します。PromiseやFutureのWeb API版です。

POST /videos/123:transcode
→ { name: "operations/abc", done: false, metadata: { progressPercent: 0 } }

GET /operations/abc   # ポーリングで進捗確認
→ { done: false, metadata: { progressPercent: 45 } }

GET /operations/abc   # 完了
→ { done: true, response: { ...変換済み動画... } }   # 成功
→ { done: true, error: { ... } }   # または失敗

設計の核

Operationは2つの型を持ちます。最終的な結果の型(Video)と、進捗を保持するメタデータの型です。状態の確認はポーリングか待機で行い、一時停止・再開・キャンセルはカスタムメソッドで行います。完了記録は原則保持しつつ、一定期間(例30日)で破棄します。

練習問題

Q1. なぜLROリソースは、操作対象となるリソースの子ではなく、トップレベルのリソースにすべきなのでしょうか?

A1. Operationは「対象リソースが作成される前」や「削除する操作」にも対応する必要があり、対象の子に置くと、対象がまだ無い/消えた場合に置き場所が無くなります。トップレベル(/operations/abc)に置けば、どの操作にも一様にアクセスでき、対象のライフサイクルから独立して進捗・結果を追えます。

Q2. なぜLROに有効期限を設定するのでしょうか?

A2. 完了した操作の記録を永久に保持すると、際限なく溜まりストレージとクエリを圧迫します。一定期間(例30日)で破棄する有効期限を設ければ、利用者が結果を取りに来る十分な猶予を与えつつ、古い操作を自動的に片付けられます。

Q3. ユーザーがLROの終了を待っているときに、カスタムの cancel メソッドで操作を中止した場合、結果はどうなりますか?

A3. cancelは処理の中止を要求し、操作は「完了(done=true)」かつ「エラー(例CANCELLED)」として決着します。すでに途中まで進んだ副作用が残ることはあり得るため、キャンセルは取り消しではない点に注意が必要で、必要なら部分結果やロールバックの扱いを明示します。

Q4. LROの進捗を記録する際、完了率を示すフィールドを持つのは良いことでしょうか?そうである理由、そうでない理由は?

A4. 進捗の目安として完了率(progressPercent)は利用者体験に有用ですが、正確な割合を出せない処理も多く、過信されると問題です。出せるなら「あくまで推定」として提供し、出せないならステージ名や処理済み件数など別のメタデータで表す方が誠実です。

第11章 再実行可能ジョブ

同じ処理を何度も実行したいのに、毎回すべての設定を渡すのは面倒です。また、クライアントではなくサーバー側のスケジューラに実行を任せたいこともあります。

ジョブと実行

設定を1回「ジョブ」というリソースとして固定し、以後はカスタムのrunで実行します。

POST /databases/123/backupJobs
  { name: "nightly", destination: "...", schedule: "毎日02:00" }
→ BackupJob リソース

# 設定再指定なしで実行(スケジューラからも)
POST /databases/123/backupJobs/nightly:run
→ Execution リソースが残る:
  /databases/123/backupJobs/nightly/executions/exec-001
  { startTime, endTime, status: "SUCCEEDED", ... }

要点

  • 「設定する人」と「実行する人」を分離できる。
  • 各実行の結果は Execution リソースとして履歴に残る。Executionは読み取り専用的(取得・一覧はできるが更新・作成はできない)。
  • LROが「1回の非同期実行」なのに対し、ジョブは「設定+繰り返し実行」を担う。runの戻り値がLROになることも多い。

練習問題

Q1. 再実行可能なジョブは、アクションを実行する権限とそのアクションを設定する権限を、どのように区別できるのでしょうか?

A1. 設定(ジョブの作成・更新)と実行(run)を別々のメソッドに分けることで、それぞれに異なるアクセス制御を割り当てられます。管理者だけがジョブを定義し、運用者やスケジューラはrunだけを呼べる、という分離が自然に表現でき、これがジョブパターンの利点の1つです。

Q2. ジョブの実行によって新しいリソースが作成される場合、その結果は Execution リソースと、新しく作成されたリソースのどちらであるべきでしょうか?

A2. runの直接の結果はExecutionリソース(その実行の記録)であるべきです。実行が生成した本来のリソースはExecutionから参照(またはLROの結果)として辿れるようにします。こうすれば「いつ・どの実行が・何を作ったか」の履歴が一貫して残ります。

Q3. Execution リソースを明示的に作成してはならない理由は何ですか?

A3. Executionは「ジョブが実行された事実の記録」であり、実行の副産物として生まれるものです。利用者がcreateで勝手にExecutionを作れると、実際には走っていない実行の偽の記録ができ、履歴の意味が壊れます。よってExecutionは読み取り専用(取得・一覧のみ)にします。


PART 4 リソース間の関係

第12章 シングルトンサブリソース

本来プロパティにすべきデータが、サイズ・複雑さ・別個のセキュリティ要件・更新頻度の違いなどの理由で、そのまま親に入れられないことがあります(例:共有ドキュメントのアクセスコントロールリスト)。

シングルトンサブリソース

プロパティとリソースのハイブリッドです。親に1つだけ紐づく子リソースとして、固有のデータを分離して保存します。

/documents/123              ← 親
/documents/123/accessControl ← 親に1つだけ存在する子(シングルトン)

要点

  • get・updateはサポートするが、create・delete はサポートしない(作成・削除されるべきでない)。代わりに既定状態へ戻す reset を備える。
  • 親リソースに付くべきで、それ自体がさらにシングルトンサブリソースを持ってはいけない。

練習問題

Q1. どのくらいの大きさであれば、属性データを別のシングルトンサブリソースに分割する意味があるのでしょうか?どんな点を考慮して決めるのでしょうか?

A1. 単純なサイズだけで決めるのではありません。データ量が大きく毎回親と返すと無駄・別個のセキュリティ要件・親と大きく異なる更新頻度・複雑な構造、といった理由があるときに分割します。これらが無ければプロパティのままで十分で、分割はコスト(別リクエスト)に見合う理由があるときだけ行います。

Q2. シングルトンサブリソースが標準メソッドを2つ(get と update)しかサポートしていないのはなぜですか?

A2. シングルトンは親と共に必ず1つ存在し、親のライフサイクルに従うため、独立してcreate/deleteされるべきではありません(親があれば常に在り、親が消えれば共に消える)。よって作成・削除は無く、参照(get)と変更(update)だけを提供します。

Q3. シングルトンサブリソースが、標準の delete を再利用せずカスタムの reset メソッドをサポートするのはなぜでしょうか?

A3. deleteは「存在しなくなる」ことを意味しますが、シングルトンは消えてはいけません。やりたいのは「既定状態に戻す」ことなので、意味の異なるdeleteを流用せず、初期化を表すresetカスタムメソッドを用意します。これで「消去」と「初期化」を取り違えずに済みます。

Q4. シングルトンサブリソースが、リソース階層内の他のシングルトンサブリソースの子になれないのはなぜでしょうか?

A4. シングルトンは「親リソースに固有の付随データ」を表す仕組みです。それをさらにシングルトンの下にネストすると階層が複雑化し、本来プロパティで足りるデータを過剰に分割することになります。シングルトンは実体リソースに直接付くべきで、入れ子にはしません。

第13章 相互参照

複数種のリソースは互いを参照し合います。参照の振る舞いは細部で解釈が分かれ、矛盾が生じえます。

指針

  • 参照を格納するフィールドは文字列型にし、接尾辞を Id にする(例 authorId)。外部リソースのコピーを保持するフィールドはこの接尾辞を付けない。
  • 参照は、参照先の存続期間ずっと有効とは期待しない。参照先が削除されれば参照は無効になりうる。
  • 参照先データをインラインで保持すると、参照元のそのデータは時間とともに古くなりうる

練習問題

Q1. 参照ではなく、外部リソースのデータのコピーを保存した方が良いのは、どのような場合でしょうか?

A1. 参照先が消えても当時の値を残したい場合(履歴・監査・スナップショット的な意味)や、参照先の可用性を問わず高速に読みたい場合です。たとえば注文時の住所や価格は、後で元リソースが変わっても「その時点の値」を保持したいのでコピーが適切です。ただしコピーは時間とともに陳腐化する点を受け入れます。

Q2. なぜAPIシステムで参照の整合性を維持しない方がよいのでしょうか?

A2. 分散したリソース間で常に整合性(参照先が必ず存在する保証)を強制すると、削除のたびに全参照を検査・連鎖更新する必要があり、スケールしません。多くのケースでは「参照先は消えうる、消えたら参照は無効」と割り切る方が現実的で、利用者にもそう期待させるべきです。

Q3. あるAPIで「参照を常に最新の状態に保つ」と保証し、その後システムが大きくなって保つのをやめることにしました。このような保証を行うことはなぜ危険なのでしょうか?

A3. 利用者はその保証に依存して設計するため、後から保証を外すと、依存していたコードが静かに壊れます。互換性の観点から、いったん与えた強い保証の撤回は破壊的変更になりがちです。だから最初から保証しすぎない(弱い保証にとどめる)のが安全です。

第14章 アソシエーションリソース

一対多なら単純なプロパティ参照で済みますが、両側が多数を持つ多対多(ユーザーとグループなど)は、使いやすく表現するのが難しくなります。

アソシエーションリソース

関係そのものを表す中間リソースで多対多をモデル化します。

User ── Membership ── Group
            └ 関係に付随するメタデータ(参加日・役割など)も保持できる

要点

  • 多対多を表す最も柔軟な方法。
  • 関係に関するメタデータを持てる。
  • 命名・標準メソッドの提供・参照整合性の扱いを整理する。

練習問題

Q1. ユーザーが参加した可能性のあるチャットルームとユーザーを関連付け、役割や参加時刻も保持するAPIを設計してください。

A1. 中間リソースMembershipを作り、/chatRooms/{room}/membershipsにuserId・chatRoomId・role・joinedAtを持たせます。Membership自体を標準メソッドでget/list/create/deleteでき、関係に付随するメタデータ(役割・参加時刻)も自然に表現できます。多対多はこの関連リソースでモデル化します。

Q2. ユーザーが同じ部屋に何度も出入りしうるが、同時に複数回参加はさせず、その履歴は保持したい場合、どうモデル化しますか?

A2.「現在の参加状態」と「履歴」を分けます。MembershipにjoinedAt・leftAtを持たせ、leftAtが空のものを「現在参加中」とみなし、その一意性制約(同一user×roomでleftAt=nullは1件まで)で二重参加を防ぎます。退出時は新規作成せずleftAtを埋め、再参加で新しいMembershipを作れば、過去の出入りが履歴として残ります。

第15章 カスタムメソッド: add、remove

関係にメタデータが要らない場合、アソシエーションリソースは「重すぎる」ことがあります。

軽量な多対多

管理側リソース(メタリソース)に対する add/remove のカスタムメソッドで、中間リソースを導入せず多対多を管理します。

POST /groups/5:addUser    { userId: "users/9" }
POST /groups/5:removeUser  { userId: "users/9" }
GET  /groups/5/users        ← 関連リストは標準 list で取得

要点

  • メタデータが不要なら、addとremoveで簡易に多対多を扱える。
  • 関連リストの取得はメタリソース上の標準listを使う。

練習問題

Q1. 2つのリソース間の多対多をモデル化するのに、アソシエーションリソースではなくカスタムの add/remove を使った方が良いのは、どんな場合でしょうか?

A1. 関係そのものに付随するメタデータ(役割・参加日など)が不要で、単に結びつきの有無だけを管理したい場合です。中間リソースは柔軟ですが重く、メタデータが無ければadd/removeのカスタムメソッドの方が軽量で扱いやすくなります。

Q2. Recipe リソースと Ingredient リソースを関連付ける場合、どちらが管理リソースで、どちらが関連付けられるリソースですか?

A2. 通常はRecipeが管理リソース(メタリソース)で、Ingredientが関連付けられる側です。POST /recipes/123:addIngredientのようにレシピに対して材料を足し引きします。「どちらに対して操作したいか(所有・集約の中心はどれか)」で管理側を選びます。

Q3. 特定のレシピを構成する Ingredient リソースの一覧を取得するのに使うメソッドは何でしょう?

A3. 管理リソース上の標準listを使います(例GET /recipes/123/ingredients)。add/removeで関係を編集し、参照の取得は通常のlistに任せるのがこのパターンの形です。

Q4. add メソッドを使って重複したリソースを追加するとどうなるでしょうか?

A4. 集合への追加なので冪等に扱い、すでに存在する関係を再度addしても二重には入らず、状態は変わりません(エラーにせず成功として扱うのが無難)。重複が意味を持たない関係では「あれば何もしない」が自然な振る舞いです。

第16章 ポリモーフィズム

種類は違うが「まとめて扱いたい」データ(チャットのテキスト/画像/音声メッセージなど)を、別々のコレクションにすると、時系列の1本の流れとして取得しにくくなります。

ポリモーフィックリソース

種類を表す型フィールドを持つ1種類のリソースにまとめます。

Message { type: "text",  content: { text: "..." } }
Message { type: "image", content: { imageUrl: "..." } }

GET /chatRooms/123/messages → テキストも画像も混在したまま時系列で返る

指針

  • 使うべきは「複数種をまとめて取得することに意味がある」場合。
  • 型情報は文字列フィールドにし、値の追加・削除は既存クライアントを壊さないよう慎重に。
  • 無効な入力はエラーにせず、必要データの有無を確認し、無関係なデータは無視する。
  • ポリモーフィックなメソッドは避ける(挙動の契約は後から変えにくいため)。リソースはポリモーフィックにしてよいが、メソッドはしない。

練習問題

Q1. WebブラウザーのURLブックマークをコレクションやフォルダーに整理するAPIで、フォルダとブックマークの2概念を1つのポリモーフィックリソースにすることは意味があるでしょうか?理由も考えてください。

A1.「同じコレクション内に混在させ、まとめて一覧・並べ替えしたい」なら意味がありますが、フォルダとブックマークは振る舞いが異なり別々に扱う場面が多いので、無理に1リソースへ統合する必要は薄いです。まとめて取得することに本質的な意味があるかで判断し、無ければ別リソース(第4章)の方が明快です。

Q2. なぜ、列挙型のようなものではなく、文字列フィールドを用いてポリモーフィックリソースの型を保持しなければならないのでしょうか?

A2. 型の種類は将来増えうるため、列挙型だと値の追加で互換性を壊しやすいからです。文字列+サーバー側検証にすれば、新しい型を後から安全に足せ、既存クライアントを壊しません。進化する集合は文字列で表すのが定石です。

Q3. なぜ追加のデータ(たとえば四角形の Shape リソース用の半径など)は、エラーとして拒否されるのではなく無視されるべきなのでしょうか?

A3. 厳格に弾くと、型の進化やクライアントの食い違いで些細な不一致でも壊れてしまい、互換性が下がります。必要なデータの有無だけを確認し、無関係なデータは寛容に無視する方が、前方・後方の互換性を保てます(ロバストネス原則)。

Q4. なぜポリモーフィックメソッドを避けるべきなのでしょうか?標準メソッド(UpdateResource() など)をポリモーフィック化するのが良くないのはなぜでしょうか?

A4. リソースはポリモーフィックでよくても、メソッドの「振る舞いの契約」は型ごとに分岐すると後から変えにくく、利用者に予測不能な挙動を強います。UpdateResource()は型ごとに全く違う動きをすると、検証・権限・副作用がばらつき、保守も難しくなります。リソースはポリモーフィックに、メソッドは単型に保つべきです。


PART 5 コレクションの操作

第17章 コピーと移動

IDや親子関係は本来不変ですが、利用者は複製・改名・別の親への移動を必要とします。これをupdate/createで代用するのは筋が悪い(IDは住所であり、createは無関係な別物になる)。

copy と move

専用のカスタムメソッドで行います。

POST /folders/123/documents/abc:move { destination: "/folders/456" }
→ 同じドキュメントが場所を変える(移動=同一性は保たれる)

POST /folders/123/documents/abc:copy { destination: "/folders/456" }
→ 新IDの別物が誕生(複製)

要点

  • 子リソースにも同じ操作を波及させる(ただしケースバイケース)。移動後の参照は最新に保つ。
  • 外部データを扱う場合、コピーが参照渡しと値渡し(コピーオンライト)のどちらかを明示する。
  • ストレージの制約を踏まえつつ、できる限りアトミックに行う。

練習問題

Q1. リソースをコピーするとき、すべての子リソースもコピーする必要があるのでしょうか?複製されるリソースを参照するリソースについてはどうでしょうか?

A1. 子リソースは原則一緒にコピーしますが、ケースバイケースで「どこまで深くコピーするか」を決めます。複製対象を参照する外部リソースについては、参照ごと複製するか、新しいコピーを指すよう貼り替えるかを明示的に設計します。曖昧にせず、コピーの境界を定義することが重要です。

Q2. APIの境界を越えて参照の整合性を維持するにはどうすればよいでしょうか?また、それは保証すべきものなのでしょうか?

A2. 外部システムをまたぐ強い参照整合性は一般に保証すべきではありません(コストが高く脆い)。必要なら、移動・コピー後に参照を更新するベストエフォートの仕組みを設け、保証ではなく「無効になりうる参照」として扱います。第13章と同様、整合性は強制しないのが現実的です。

Q3. データをコピー・移動するとき、得られるデータが意図どおりの真のコピーであり、APIを使う他の人が変更したデータではないと、どう確認できるでしょうか?

A3. 操作をできる限りアトミックに行い、コピー時点のスナップショット(またはリビジョン、第28章)を基準にします。途中で他者の変更が混ざらないよう一貫した時点のデータを使い、必要なら版識別子やエタグで「どの時点を写したか」を明示して検証できるようにします。

Q4. 異なるセキュリティとアクセス制御のポリシーを持つ親へリソースを移動させた場合、古いものと新しいもの、どちらのポリシーが適用されるべきでしょうか?

A4. 移動先の新しい親のポリシーが適用されるべきです。リソースは新しい場所(住所)に属するので、移動後はその場所の規則に従います。これにより「移動して権限の制約を回避する」抜け道を防げます。移動の最中は、移動元・移動先双方の権限を確認するのが安全です。

第18章 バッチ操作

複数リソースを個別に呼ぶとN回の往復が発生し、しかも一部だけ成功する中途半端な状態が起きえます。

バッチ操作

1回の呼び出しでまとめて操作します。

POST /chatRooms/123/messages:batchGet { ids: ["msg-1","msg-2","msg-3"] }
→ 3件まとめて、要求順に返る

要点

  • 命名は Batch<Method><Resource>()全部成功か全部失敗かのアトミック(一部成功を許さない)。
  • 対象は親ではなくコレクション(.../messages:batchUpdate)。
  • 結果は要求と同じ順序で返す(位置で対応づけられる)。
  • 共通フィールドは括り出して繰り返しを避ける。親をまたぐ場合はハイフン - をワイルドカードにし、各リクエストにフルパスを書く。

練習問題

Q1. バッチメソッドの結果が特定の順番であることが重要なのはなぜでしょうか?順番が違っていたらどうなるのでしょうか?

A1. 結果を要求と同じ順序で返せば、利用者は位置(インデックス)で要求と結果を対応づけられます。順序が保証されないと、どの結果がどの要求のものか分からず、IDで突き合わせる余計な処理が必要になり、欠落や取り違えのもとになります。

Q2. 一括更新リクエストで、リクエストの parent フィールドが更新されるリソースの親フィールドと一致しない場合、どんなレスポンスを返すべきでしょうか?

A2. 不整合はエラーとして拒否すべきです(例400 Bad Request)。バッチはアトミックなので、1件でも親の不一致があればバッチ全体を失敗させ、部分的に適用しないようにします。親をまたぐ場合はワイルドカード - を使い、各リクエストにフルパスを書かせて整合を取ります。

Q3. なぜバッチリクエストはアトミックであることが重要なのでしょうか?一部が成功し他が失敗しうる場合、APIの定義はどうなるのでしょうか?

A3. 一部だけ成功すると中途半端な状態になり、利用者はどこまで適用されたか分からず復旧が困難です。全部成功か全部失敗(アトミック)にすれば結果が明快です。どうしても部分成功を許すなら、それはバッチではなく各要素の個別結果(成功/失敗)を返す別物として定義し、その旨を明示する必要があります。

Q4. バッチ型 delete メソッドが HTTP DELETE ではなく HTTP POST を利用しているのはなぜでしょうか?

A4. HTTP DELETEはボディを持たせにくく、削除対象IDのリストのような複雑な入力を運べません。POSTなら本体に対象の一覧や条件を載せられます。バッチや条件削除は「コレクションに対するアクション」としてカスタムメソッド(POST + コロン記法)で表すのが自然です。

第19章 条件に基づく削除

バッチ削除はIDが分かっている前提ですが、「特定の条件にマッチするものを全部消したい」こともあります。これは非常に危険です。

purge メソッド

フィルター条件にマッチするリソースをまとめて削除します。

POST /messages:purge
  { filter: "createTime < '2020-01-01'", validateOnly: true }
# 「約1,432件が削除されます(サンプルはこちら)」を返す
# (実際には消さない=ドライラン)

要点

  • 危険なので、本当に必要なときだけ提供する。
  • 既定では実際に削除せず検証(ドライラン) として振る舞う。
  • レスポンスに影響件数(と一部サンプル) を含める。
  • 一貫性は標準listと同じガイドラインに従う。

練習問題

Q1. purge メソッドが、絶対に必要な場合だけに限定されるべきなのはなぜでしょうか?

A1. 条件に一致するものを一括で消すのは極めて危険で、フィルタの誤りで意図せず大量のデータを失いかねません。標準のdeleteやバッチdeleteで足りるなら不要で、本当に「条件指定の一括削除」が必須なときだけ提供すべきです。

Q2. なぜ purge メソッドのデフォルトは検証の実行のみなのでしょうか?

A2. 取り返しのつかない一括削除を、うっかり本実行してしまう事故を防ぐためです。既定を検証(validateOnly相当)にして「何件消えるか」を返すだけにし、本当に消すには明示的なフラグを必要とすることで、安全になります。

Q3. フィルターの基準が空のままだと何が起きますか?

A3. 空フィルタを「全件一致」と解釈すると、全データ削除という最悪の事故になります。よって空フィルタは安全側に倒し、エラーにする(何も対象にしない、もしくは明示的な「全件」指定を別途要求する)べきです。既定でドライランなのも併せて被害を防ぎます。

Q4. リソースが論理削除をサポートしている場合、purge メソッドはどう振る舞うべきでしょうか?論理削除すべきか、物理削除すべきか?

A4. 一貫性のため、purgeも通常のdeleteと同じ意味(論理削除をサポートするなら論理削除)にするのが基本です。物理的に消すのはexpungeの役割なので、purgeでは論理削除に倒し、完全消去は別途expunge相当で行う設計が自然です。

Q5. 影響を受けるリソースの個数を返すのは何のためでしょうか?マッチするリソースのサンプルセットについてはどうですか?

A5. 影響件数は、本実行前に「これだけ消える」と利用者へ規模を知らせ、誤った広すぎるフィルタに気づかせるためです。マッチのサンプルを併せて返せば、「本当に意図した対象か」を具体的に確認でき、ドライランの安全性がさらに高まります。

第20章 匿名書き込み

ログや解析イベント、メトリクスのように、後から個別に特定する必要のない大量データに対し、住所付きリソースを1件ずつcreateするのは無意味で無駄です。

write メソッド

リソースを作らず、データ(エントリー)を書き込みます。

POST /events:write
  { entry: { type: "click", page: "/home", timestamp: "..." } }
# レスポンスは 200 OK のステータスだけ(IDもリソースも返らない)

要点

  • 書き込みは一方通行で、後から個別に取得・削除できない。
  • レスポンスはステータスコードのみ。LROでもリソースを返さない(IDを返すと「アドレス指定可能」と矛盾する)。
  • 扱う単位は「リソース」ではなく、アドレスを持たず多くは一時的な「エントリー」。
  • 取り込んだデータの一貫性(遅延・結果整合性)に注意する。

練習問題

Q1. write メソッドで重複したデータを取り込みたくない場合、これを回避する最も良い方法は何でしょうか?

A1. クライアント生成のリクエスト識別子(第26章の冪等キー)を各エントリーに付け、サーバーが既知のIDを重複として弾く方法が有効です。匿名書き込みは後から個別に消せないので、入口で冪等性を効かせて重複取り込みを防ぐのが定石です。

Q2. なぜ write メソッドはレスポンスでボディを返してはいけないのでしょうか?write メソッドが LRO リソースを返すのは、なぜ良くないのでしょうか?

A2. writeは「アドレスを持たない匿名エントリー」を書く操作なので、IDやリソース表現を返すと「アドレス指定可能」という前提と矛盾します。LROを返すと、そのOperationの結果として書いたものを参照でき、結局アドレスを与えてしまうため不適切です。返すのはステータスコードだけにします。

Q3. データは受け取ったが処理がまだされていないことを伝えたい場合、どんな方法があるでしょうか?ほとんどのAPIで最も良い方法は?

A3.「受理したがまだ処理していない」はHTTP 202 Acceptedで表すのが一般的です。ほとんどの匿名書き込みでは、202(または単純な200)でステータスだけ返し、結果整合で後追い処理する形が最良です。個別追跡が要るならエントリーではなくリソース化を検討します。

第21章 ページ分割

10億件・100GBのような大きなデータは、1回のレスポンスに収まりません。

3つのフィールド

maxPageSize / pageToken / nextPageToken で小分けに取得します。

GET /messages?maxPageSize=50
→ { messages: [...50件...], nextPageToken: "abc" }   ← 続きあり

GET /messages?maxPageSize=50&pageToken=abc
→ { messages: [...次の50件...], nextPageToken: "def" }

GET /messages?maxPageSize=50&pageToken=...
→ { messages: [...残り...] }   ← nextPageToken が無い=終わり

要点

  • サイズは「正確」ではなく「最大」(揃う前でも早めに返せる)。
  • 終了判定は「結果が空」ではなく「nextPageToken が無い」(空でも続きがありうる)。
  • ページトークンは中身を見ない不透明な黒い箱として扱う。有効期限・安全性も設計する。
  • 単一の巨大リソース内のページングにも使える。
  • offset/limit は避ける(深いoffsetは遅い、挿入・削除で重複/欠落が起きる)。

練習問題

Q1. 正確なページサイズではなく、最大ページサイズを使うことが重要な理由は何でしょうか?

A1.「最大」にしておけば、サーバーは全件揃う前でも用意できた分だけ早く返せ、タイムアウトや無駄な待ちを避けられます。正確な件数を常に満たそうとすると、内部の都合(シャード差・フィルタ)で性能が落ちます。利用者は「最大N件、足りなければ少なく返る」と理解すべきです。

Q2. リクエスト内のページサイズフィールドが空白/負の値/0/巨大な数値の場合、それぞれどうなるべきでしょうか?

A2. 空白は既定値を使い、負や0は不正としてエラーにする(または既定に丸める)べきです。巨大な値はサーバー側の上限(例100)にクランプします。いずれも無制限を許さず、安全な範囲に正規化することで、過大なレスポンスやDoSを防ぎます。

Q3. ページ分割が完了したことを利用者が知るにはどうしたらよいでしょうか?

A3. レスポンスにnextPageTokenが無いことを「終わり」の合図とします。「結果が空=終わり」と判断してはいけません(途中で空ページが返ることもあるため)。トークンの有無だけで継続/終了を判定します。

Q4. ある種類のリソースのページトークンが、他の種類のリソースと異なる有効期限を持つことは問題ないでしょうか?

A4. 問題ありません。ページトークンは不透明で、各リソース種・各クエリの事情に応じて有効期限を決めてよいものです。利用者はトークンの中身や寿命に依存すべきでないので、種類ごとに有効期限が違っても構いません。

Q5. ページトークンが利用者にとって完全に不透明であることが重要な理由を述べ、どうすればうまく実現できるか答えなさい。

A5. 中身を公開すると利用者がそれに依存し、内部実装(オフセット方式や並び順)を変えられなくなるうえ、改ざんで不正なクエリを作られる恐れもあります。トークンは「黒い箱」とし、サーバー側でエンコード(必要なら署名・暗号化)して、利用者はそのまま返すだけにするのが望ましい実現です。

第22章 フィルタリング

get(ID既知)とlist(全部)の中間、「条件に合うものだけブラウズしたい」という要求に応える手段が要ります。

filter フィールド

listに文字列のフィルターを足します。

GET /chatRooms/123/messages?filter=author = "users/5" AND read = false

要点

  • フィルタは構造化インターフェースではなく、SQLライクな文字列で渡す(表現力と進化性が高い)。
  • 評価コンテキストは単一リソースに限る(他リソース参照やJOINを許すと実行時間が無制限に膨らむ)。
  • 配列は位置でなくメンバーシップで判定("x" IN tagstags[0] は不可)。
  • フィルタのエラーは黙殺せず即座に返す(誤った結果を本物と誤認させない)。
  • 基本比較で足りなければ、ドキュメント化した関数群lower(), starts_with() など)を提供する。

練習問題

Q1. フィルター条件に構造化インターフェースを使うことの主な欠点は何ですか?

A1. 構造化(JSONのAND/ORツリーなど)は、表現したい条件が増えるたびにスキーマを拡張せねばならず硬直的で、進化させにくい点が欠点です。SQLライクな文字列なら、サーバー側のパーサを拡張するだけで新しい演算子・関数を足せ、表現力と進化性で勝ります。

Q2. 配列フィールドの値の位置でフィルタリングできるようにすることは、なぜ良くないのでしょうか?

A2. 配列はアトミックな集合として扱うべきで、要素の位置(添字)は同時編集や並べ替えで簡単にずれるからです。tags[0]= “x” のような位置指定は壊れやすく、メンバーシップ(“x” IN tags)で判定するのが正しい設計です。

Q3. 存在するか分からないマップキーはどう扱えばよいのでしょうか?存在しないキーと比較する場合、厳格にエラーを出すべきか、存在しないものとして扱うべきか?

A3. 厳格にエラーにせず、「存在しないキー=値が無い(条件に一致しない)」として寛容に扱うべきです。エラーで弾くと、データのばらつきでフィルタが頻繁に失敗し使いにくくなります。無いものは静かに不一致とみなす方が頑健です。

Q4. 利用者が、フィールドのサフィックスに基づいてリソースを絞り込みたい(例 名前が "man" で終わる人を検索)とします。これはフィルタリング文字列でどう表現されるのでしょうか?

A4. 提供する関数を使い、たとえばends_with(name, “man”)のように書きます。基本比較で足りない接尾辞一致のような要求には、ドキュメント化した関数(starts_with()・ends_with()・lower() など)を用意しておき、それをフィルタ文字列内で呼べるようにします。

第23章 インポートとエクスポート

大量のリソースを、外部ストレージ(S3/GCSなど)との間で効率よく出し入れしたい。クライアント経由で流すのは非効率です。

API が外部ストレージと直接やり取りする

クライアントは場所を指示するだけで、API側が直接ストレージに接続します。

POST /products:import
  { inputConfig: { gcs: { uri: "gs://bucket/products.csv" } } }
POST /products:export
  { outputConfig: { gcs: { uri: "gs://bucket/export.json" } } }

設計の核:直交する2つのインターフェース

  • ストレージ側:バイト列をどこと・どう出し入れするか(S3/GCS、URI、認証)。
  • 変換(シリアライズ) 側:そのバイト列をAPIのリソース表現とどう相互変換するか(CSV/JSON、列とフィールドの対応)。

この2つは独立しているので自由に組み合わせられ、柔軟性と再利用性が高まります。

要点

  • バックアップ/リストアとは別物。エクスポートは完璧なスナップショットではなく、インポートは新IDで新規作成しがち。
  • ポイントインタイム読み取りがない限り、扱うデータは最新とは限らない(時間幅でにじむ)。
  • 原則1リソース型ずつ。子・参照リソースの厳密な扱いはバックアップ/リストアに委ねる。

練習問題

Q1. なぜ import/export メソッドの設定に2つの別々のインターフェースを使うことが重要なのでしょうか?なぜ、これらを1つのインターフェースにまとめないのでしょうか?

A1.「どこに・どう出し入れするか(ストレージ:S3/GCS、URI、認証)」と「バイト列をリソース表現とどう変換するか(形式:CSV/JSON、列対応)」は本来独立した関心事だからです。直交させておけば、ストレージと形式を独立に選べるので、組み合わせの自由度と再利用性が高まります。1つに混ぜると組み合わせのたびに重複が生じます。

Q2. 最新でないデータをインポート(またはエクスポート)しないようにするには、どんな選択肢がありますか?どれが最も良い方法でしょうか?

A2. ポイントインタイム読み取り(ある時点の一貫スナップショット)をサポートするのが最も確実です。それが無ければ、エクスポート中の書き込みを止める、リビジョンやタグで時点を固定する、などの手段があります。最良はスナップショット読み取りで、これが無い限り「最新とは限らない」前提を受け入れます。

Q3. リソースをエクスポートする際、識別子も一緒に保存する必要がありますか?同じデータをインポートする場合、新しいリソースは同じ識別子を持つべきでしょうか?

A3. import/exportはバックアップ/リストアとは別物なので、識別子の保存は保証されず、インポートは通常「新しいID」で新規作成しがちです。同一IDの保持が必要なら、それはリストアの領域です。エクスポートに識別子を含めるかは設計判断ですが、インポートで同一IDを再現できる前提に立つべきではありません。

Q4. エクスポートに失敗した場合、これまでに転送されたデータは削除されるべきでしょうか?なぜそうすべきか、またはそうすべきでないのでしょうか?

A4. 一概には消しません。エクスポート先は外部ストレージで、部分的に書かれたファイルの扱いは利用者やストレージの方針に依存するからです。中途半端な出力が誤って使われないよう、未完了を示す印(一時名・マーカー)を付け、完了時にコミットする方式が安全です。自動削除はデータ消失リスクとのトレードオフで決めます。

Q5. 子リソースやその他の関連リソースを含むインポート/エクスポートをサポートしたい場合、どうすればよいでしょうか?識別子はどう扱われるべきでしょうか?

A5. 原則は1リソース型ずつですが、関連まで含めたいなら、それはバックアップ/リストアに近い機能として設計します。子・参照を含める場合、識別子の対応関係(旧ID→新IDのマッピング)を保持・再構築する仕組みが必要で、参照整合性をどう保つかを明示します。安易にimport/exportを拡張せず、専用のバックアップ機構に委ねるのが無難です。


PART 6 安心と安全

第24章 バージョンと互換性

ソフトは進化しますが、Web APIは硬直的で公開されているため、変更が既存利用者を壊しがちです。これを避けるのがバージョン管理です。

互換性・後方互換性

互換性とは、ある版向けのコードが別の版でも動くこと。後方互換とは、前の版向けのコードが新しい版でも期待通り動くことです。

厄介な点

「期待通り」は解釈次第です。バグ修正のような一見無害な変更すら、誰かを壊しうる(例:バグを補正していた利用者が、修正で二重補正になる)。何が後方互換かは、利用者が実際に何に依存しているかで変わります。

対処:スペクトルとポリシー

互換性を白黒でなくスペクトルととらえ、「何を保証し、何は保証しないか」を一貫した互換性ポリシーとして明文化します。

バージョニング方式とトレードオフ

URLパス:   /v1/users, /v2/users      単純・可視・普及。だが粒度が粗い
ヘッダー:   API-Version: 2024-06-01   URLが綺麗・細かい。だが見えにくい
日付ベース: Stripe方式               細かく段階的・満足度高。だが維持が複雑

粒度・単純さ・安定性・新機能・満足度・普及度のトレードオフに帰着し、万能解はありません。

練習問題

Q1. フィールドのデフォルト値の変更は、後方互換性があるとみなされますか?

A1. 一般に後方互換ではありません。既定値に依存していた利用者は、値を変えると挙動が静かに変わるからです。一見無害でも「省略時にこうなる」前提に依存するコードを壊しうるので、破壊的変更として扱うのが安全です。

Q2. 大手商業銀行は安定性を何より重視し、小規模な利用者の多くはより頻繁な更新や新機能を望んでいます。このようなニーズのバランスを取るにはどうしたらよいでしょうか?

A2. 複数バージョンを併走させ、安定版を長期サポートしつつ新版で新機能を出すのが定石です。明文化した互換性ポリシーで「何を・どれだけの期間保証するか」を示し、安定を望む顧客は旧版に留まり、先進性を望む利用者は新版へ移れるようにします。日付ベースのバージョニングも段階移行に有効です。

Q3. API公開直後にフィールド名の恥ずかしいタイプミス(port_number が porn_number)に気づき、すぐ直したいとします。バージョン番号を変えずに変更するか決める前に、何を考慮すべきでしょうか?

A3. たとえ誤字でも、既に誰かがその名前に依存していれば変更は破壊的になります。考慮点は「公開後どれだけ経ったか・実利用者がいるか・どれだけ広まったか」です。実質的に誰も使っていない極初期なら同一バージョンで直し、依存者がいるなら新バージョンか、旧名のエイリアス併存で移行期間を設けます。

Q4. 必須フィールドの欠落時に 400 Bad Request ではなく誤って 200 OK(空レスポンス)を返すバグがあるとします。同じバージョンで修正してよいか、次のバージョンか?セマンティックバージョニングに従うとメジャー・マイナー・パッチのどれですか?

A4. これは仕様違反のバグ修正で、正しい400を返すのが本来の契約です。原則は同一バージョンで修正してよいですが、誤った200に依存した利用者がいれば壊れる恐れがあります。セマンティックバージョニングでは、観測される振る舞いを変える後方非互換な修正はメジャー、互換を保てるならパッチ相当と判断します(依存実態しだいでメジャー扱いが安全)。

第25章 論理削除

標準deleteの物理削除はやり過ぎなことが多く、「ごみ箱」のように復元可能な削除をしたい場面があります。

論理削除(ソフトデリート)

実体を消さず「削除済み」の印をつけます。

DELETE /users/5 → 消さずに deleted: true にする(実体は残る)

標準メソッドへの調整

  • 論理削除済みリソースへの get は 404 を返さない(存在はする)。
  • list は既定で隠すが、showDeleted のようなオプションで見せられる。

復元と完全削除

POST /users/5:undelete  → 復元(deleted: false)
POST /users/5:expunge   → 物理削除(本当に消す、復元不可)

保持期間が過ぎたあとに自動でexpungeする設計もあります。

要点

  • カスケード削除など参照整合性は物理削除と一貫させる。
  • コスト:ストレージ増、クエリ複雑化、一意性制約(削除済みが枠を占有)、プライバシー(本当に消す要求にはexpungeが必要)。

練習問題

Q1. リソースが論理削除されたことを表すのに、ブール型フラグと状態フィールドの使い分けが意味を持つのはどんな場合でしょうか?同じリソースに両方を持つことは意味がありますか?

A1. 状態が「有効/削除済み」の2値ならブール(deleted)で十分ですが、「下書き/公開/アーカイブ/削除済み」のように状態が複数あるなら状態フィールド(enumでなく文字列)が適します。両方を持つと「削除済み」を二重に表現でき矛盾の温床になるため、通常は片方に統一すべきで、両立は基本的に意味がありません。

Q2. 標準の list メソッドで、論理削除されたリソース『だけ』を取得したい場合、利用者はどう指定すればよいのでしょうか?

A2. showDeletedのような「既定では隠す対象を見せる」オプションに加え、フィルタ(第22章)でdeleted = trueのように指定して削除済みだけに絞ります。listは既定で削除済みを隠すので、明示的なフラグ/フィルタで対象を切り替えます。

Q3. 論理削除されていないリソースにカスタムの expunge を呼ぶと何が起こりますか?同じリソースに undelete を呼ぶとどうなりますか?

A3. expungeは物理削除なので、論理削除済みかどうかに関わらず実体を完全に消せる設計が一般的です(ごみ箱を経ず直接消す)。一方undeleteは「削除済みを戻す」操作なので、削除されていないリソースに呼んでも何も変わらない(冪等に成功扱い、またはエラー)とします。

Q4. 論理削除されたリソースが失効するのはいつがよいのでしょうか?有効期限はどのように示されるべきでしょうか?

A4. 一定の保持期間(例30日)を過ぎたら自動的にexpungeする設計が分かりやすいです。各リソースにexpireTime(いつ完全削除されるか)を持たせて利用者に示し、期限到来で物理削除します。これでごみ箱の自動清掃と、復元可能な猶予を両立できます。

Q5. どんな状況であれば、論理削除のサポートを追加することは後方互換性を持った変更になるのでしょうか?後方互換性のない場合についてはどうでしょうか?

A5. deleteの観測可能な振る舞いが変わらなければ後方互換です。たとえば従来どおり「delete後にgetすると404」を保ったまま内部的に論理削除するなら互換的です。一方、delete後もgetが200を返したり、listの見え方が変わったりするなど、利用者から見える挙動の変化は後方非互換になります。

第26章 リクエストの重複実行回避

応答が途中で失われ、クライアントが再送すると、冪等でない操作(課金など)が二重実行されます。

リクエスト識別子(冪等キー)

クライアントが1回ぶんに固有のIDを付け、サーバーはそれをキーに結果を覚えて、同じIDの再送には実行し直さず保存済みの結果を返します。

POST /payments { amount: 100 }  Request-Id: abc123
→ 初回:課金して結果を保存(応答は消失)
再送 同じ Request-Id: abc123
→ 「abc123 は処理済み」→ 課金せず保存済み結果を返す(課金は1回だけ)

要点

  • IDはクライアントが生成する(再送で同じIDを付けられるのはクライアントだけ)。
  • 結果はキャッシュされる。有効期限・無効化を慎重に決める。
  • IDの衝突に備え、リクエスト内容のフィンガープリント(SHA-256など) も併せて照合する。同じID×違う指紋ならエラーで弾く。

練習問題

Q1. リクエストのフィンガープリント(ハッシュ値)を使って重複を判断するのが良くないのは、なぜでしょうか?

A1. 内容が同一でも「意図的に2回実行したい」正当な要求(同額の支払いを2回など)まで重複とみなして弾いてしまうからです。だから重複判定はクライアント生成のリクエスト識別子で行い、フィンガープリントはあくまで「同じIDに違う内容が来た」衝突検出の補助に使います。

Q2. なぜリクエスト/レスポンスのキャッシュを最新に保とうとするのは良くないのでしょうか?キャッシュされたレスポンスは、元リソースが変化したら無効化・更新されるべきではないのでしょうか?

A2. このキャッシュは「同じリクエストに同じ結果を返す」冪等性が目的で、元リソースの最新値を映す汎用キャッシュではありません。元が変わったからと更新してしまうと、再送に対して初回と違う結果を返し、冪等性が崩れます。だから一定期間は初回の結果をそのまま保持します。

Q3. 重複をチェックするキャッシュシステムがダウンした場合、何が起こるのでしょうか?どんな障害シナリオが考えられ、どう保護すべきでしょうか?

A3. キャッシュ喪失中は重複を検出できないため、再送が二重に実行されかねません。対策としては、キャッシュの永続化・冗長化、ダウン時に安全側へ倒す仕組み、下流での一意制約といった多層防御を組み合わせます。単一のキャッシュに全面依存しない設計が重要です。

Q4. すでにリクエスト識別子があるのに、リクエストのフィンガープリントを使うことがなぜ重要なのでしょうか?識別子生成のどんな性質がこの要件につながるのでしょうか?

A4. リクエストIDはクライアントが生成するため、別内容のリクエストに誤って同じIDを付けてしまう(衝突)可能性があります。フィンガープリントを併せて照合すれば、「同じIDなのに内容が違う」異常を検出してエラーにできます。クライアント生成=中央で一意性を保証できない、という性質がこの二重照合の必要性を生みます。

第27章 リクエストの検証

安全な操作は「試せば」分かりますが、アカウント閉鎖・大量削除・課金のような危険な操作は、試すと本当に起きてしまいます。

validateOnly(ドライラン)

リクエストにブール型の validateOnly を足します。trueなら本番と同じ検証を走らせ「どうなるか」を報告するが、副作用は起こさない

POST /accounts/123:closeAccount { validateOnly: true }
→ 「実行すれば成功する/○○で失敗する」を返すが、実際には閉じない

要点

  • 検証リクエストは安全(副作用なし)で、何度でも試せる。本番と同じ検証ロジックを通すので予告が正確。
  • 外部サービス依存はベストエフォートで検証(副作用は起こさず、全部は検証しきれないと認める)。
  • 採番などの特殊な副作用は実際には起こさず、必要ならシミュレートして報告する。
  • これは予測であって保証ではない(検証後に状態が変わりうる)。

練習問題

Q1. リクエストを検証する個別のメソッドを用意するのではなく、「検証のみ」とするフラグを使うのはなぜでしょうか?

A1. 操作ごとに別メソッドを増やすと、本番ロジックと検証ロジックが二重化し、ずれて「検証は通ったのに本番で失敗」が起きます。同じメソッドにvalidateOnlyフラグを足せば、まったく同じ検証経路を通せて予告が正確になり、APIの表面積も小さく保てます。

Q2. リモートサービスからデータを取得する API メソッドを想像してください。検証リクエストもリモートサービスと通信すべきでしょうか?そうすべき理由、そうすべきでない理由は?

A2. 副作用を起こさない範囲でのベストエフォートにとどめます。到達性や認可の確認のため通信してよいですが、外部の副作用を起こしてはならず、また外部状態は検証後に変わりうるので「すべては保証できない」と認めます。安全に確認できる部分だけ検証します。

Q3. データを書き込まないメソッドで検証リクエストをサポートすることは、意味があるのでしょうか?

A3. ほとんど意味がありません。読み取り専用の操作はそもそも安全に「試せる」ので、わざわざvalidateOnlyを設ける必要はありません。検証フラグは、実行すると危険な副作用を伴う操作にこそ価値があります。

Q4. validateOnly フラグのデフォルト値が false であることが重要なのはなぜですか?デフォルト値を反転させることに意味はあるでしょうか?

A4. 既定falseなら、フラグを意識しない普通の呼び出しが本来の実行になり、直感に合います。もし既定true(検証)にすると、利用者が「実行したつもりが何も起きていない」という分かりにくい事故を招きます。purgeのような特に危険な操作を除き、既定を反転させるべきではありません。

第28章 リソースリビジョン

通常は「今の状態」しか保存せず、過去は捨てられます。でも履歴を見たい・巻き戻したい場面があります(Googleドキュメントの版履歴やgitのように)。

リビジョン = 時点ごとのスナップショット

/documents/1234        ← 本体(現在=アクティブ)
/documents/1234@5678   ← 特定リビジョンの取得(@記法)

要点

  • リビジョンIDは連番やタイムスタンプでなくランダムにする(情報漏れ・衝突を避ける)。
  • 作成は暗黙的(変更のたび)か明示的(要求に応じて)。
  • 取得は @記法。だが一覧と削除はカスタムメソッドで行う(:listRevisions, :deleteRevision)。
  • 巻き戻しは履歴を壊さず、過去と同じ内容の新リビジョンを作ってアクティブにする(追記専用で監査証跡が残る)。
  • リビジョン対象の子リソースをどう扱うかは設計判断。

練習問題

Q1. どんなユースケースなら、暗黙的にリビジョンを作成するのに適しているのでしょうか?明示的に作成する場合はどうでしょうか?

A1. 変更のたびに自動で版を残したい場合(ドキュメント編集の履歴のように、すべての変更を追える)は暗黙的が適します。一方、利用者が「ここを確定版にする」「タグを打つ」ような区切りで版を残したい場合は明示的(要求に応じて作成)が適します。粒度と利用者の意図で選びます。

Q2. なぜリビジョンは、数字が増えるだけのものやタイムスタンプではなく、ランダムな識別子を使うべきなのでしょうか?

A2. 連番は総数や作成ペースが漏れ、推測で他の版を辿られます。タイムスタンプは同時刻で衝突し、やはり情報が漏れます。ランダムIDなら情報漏れと衝突を避けられ、安全です(識別子の一般原則と同じ、第6章)。

Q3. 復元する際には、なぜ新しいリビジョンを作成するのでしょうか?なぜ以前のリビジョンを参照しないのでしょうか?

A3. 履歴を追記専用に保つためです。過去の版を直接「現在」に差し替えると履歴が壊れ、監査証跡が失われます。復元は「過去と同じ内容の新リビジョンを作ってアクティブにする」ことで、すべての操作が前向きに記録され、いつでも巻き戻しを追跡できます。

Q4. 以前のリビジョンに復元しようとした際、そのリソースがすでに前のリビジョンと同じ内容だった場合は、どうすればよいのでしょうか?

A4. 変更が無いので、新しいリビジョンを作らず何もしない(冪等に成功を返す)のが妥当です。同一内容の版を量産しても無意味なため、「現在と同じならno-op」と扱います。

Q5. リソースを取得するのと削除するのに、標準メソッドではなくカスタムメソッドを使うのはなぜでしょうか?

A5. リビジョンの一覧・削除は、通常のリソース操作とは意味が異なる特別な操作だからです。取得自体は @記法で標準的に行えますが、版の列挙(listRevisions)や特定版の削除(deleteRevision)は履歴という特殊対象への操作なので、標準list/deleteを流用せずカスタムメソッドで明示します。

第29章 リクエストの再試行

リクエストは失敗します。再試行すれば成功する一時的な失敗と、何度やっても無駄な恒久的失敗があり、闇雲な再送は群れの殺到を招きます。

どのエラーを再試行するか

# 再試行する(一時的なエラー)
429 Too Many Requests, 503 Service Unavailable, タイムアウト

# 再試行しない(恒久的なエラー)
403 Forbidden, 400 Bad Request, 401 Unauthorized, 404 Not Found

恒久的エラーはリクエスト自体が制約に反しているので、同じ送り直しは永遠に失敗します。

指数バックオフ

すぐ連打せず、待ち時間を倍々に増やします(1s→2s→4s→8s…)。回数と最大待ち時間に上限を設けます。

ジッターとthundering herd問題

全クライアントが同じ規則だと、同時刻に殺到して復旧中のサーバーを再び落とします。ジッター(ランダムなばらつき) を混ぜて再試行を分散させます。(thundering herd問題: 復旧しかけたサーバーに1万件もの同時リクエストの波が繰り返し襲い、また落としてしまうような問題)

Retry-After

サーバーが再試行時刻を知っているなら Retry-After ヘッダーで伝え、クライアントはそれに従います。

練習問題

Q1. 失敗したリクエストを安全に再試行できるかを決める単純なルールが存在しないのは、なぜでしょうか?

A1. 同じステータスコードでも、操作が冪等か・どこまで進んだか・副作用があるかで、再送して安全かどうかが変わるからです。たとえばタイムアウトは「届いて処理済みかも」しれず、課金のような非冪等操作では二重実行になりえます。だから一律のルールでは決められず、冪等性やリクエスト識別子(第26章)と併せて判断します。

Q2. 指数バックオフを利用する根本的な理由は何ですか?再試行間のランダムジッターの目的は何ですか?

A2. 失敗直後に連打すると、復旧しかけのサーバーへ負荷が集中して再び落とします。待ち時間を倍々に伸ばす指数バックオフで負荷を逃がします。さらに全クライアントが同じ間隔だと同時刻に殺到(thundering herd)するため、ジッター(ランダムなばらつき)で再試行時刻を散らして同期を崩します。

Q3. Retry-After ヘッダーの使用は、どんなときに意味を持つのでしょうか?

A3. サーバー側が「いつ再試行すべきか」を知っている場合(レート制限の解除時刻、メンテ明けなど)に意味があります。429や503と共にRetry-Afterを返せば、クライアントは当てずっぽうのバックオフより正確なタイミングで再送でき、無駄な試行を減らせます。

第30章 リクエストの認証

届いたリクエストが、本当に正規利用者からの本物で、改ざんされていないと、どう保証するか。

認証の3要件

  • 発信元(origin):名乗り通りの既知の送信元から来た証明。
  • 整合性(integrity):送信後に改ざんされていない証明。
  • 否認防止(non-repudiation):後から「自分じゃない」と言い逃れできなくする。

身元 = 秘密鍵の所有

現実の身元と違い、認証では「秘密鍵を持っていること」を身元とします。無記名債券のように、鍵を持つ者が本人とみなされます(だから鍵の秘匿がすべて)。

デジタル署名(公開鍵/秘密鍵)

クライアント: 秘密鍵で署名 / サーバー: 公開鍵で検証(公開鍵は事前登録)

# fingerprint = リクエスト全体の指紋
fingerprint = hash(メソッド + パス + ホスト + ボディ)
signature   = sign(fingerprint, 秘密鍵)

# サーバーは受信内容から指紋を再計算し、公開鍵で署名を検証
→ OKなら:本人しか署名できない(発信元)+ 指紋一致(整合性)

要点

  • 署名対象はメソッド・パス・ホスト・ボディを含む全体の指紋(一部だけだと残りを改ざんされる)。
  • HMAC/共有秘密鍵でも多くは足りるが、サーバーも同じ鍵を持つため第三者への否認防止が成り立たない。秘密鍵を本人しか持たないデジタル署名ならそれが可能。
  • 認証情報は生成→登録(公開鍵)→署名→検証の流れ。署名の詳細(鍵・署名・アルゴリズム)はヘッダーで伝える。

練習問題

Q1. リクエストの発信元を証明することと、将来起こりうる否認を防止することの違いは何でしょうか?なぜ前者は後者をカバーできないのでしょうか?

A1. 発信元の証明は「いま受け取ったリクエストが、名乗り通りの送信者から来た」ことの確認です。否認防止は「後から本人が『送っていない』と言い逃れできない」ことの保証で、第三者にも証明できる必要があります。共有秘密などで発信元を確認できても、検証者自身が同じ鍵で偽造できるなら第三者に送信者を立証できず、だから前者だけでは後者を満たせません。

Q2. クライアントとサーバーの間で共有される秘密鍵では、どの要件が満たされないのでしょうか?

A2. 否認防止が満たされません。サーバーも同じ秘密鍵を持つため、そのサーバーが署名を偽造でき、「本人しか作れない署名」と第三者に示せないからです。発信元と整合性は満たせても、否認防止には本人だけが持つ秘密鍵による公開鍵署名が必要です。

Q3. リクエストのフィンガープリントに HTTPメソッド・ホスト・パスの属性が含まれることが、なぜ重要なのでしょうか?

A3. 署名対象がボディだけだと、メソッドやパスを差し替えて同じ署名を悪用できてしまうからです(例同じボディでDELETEに変える、別ホストへ向ける)。メソッド・ホスト・パス・ボディ全体を指紋に含めて署名することで、リクエストのどの部分も改ざんできないようにします。

Q4. このパターンで示されるデジタル署名は、リプレイ攻撃の影響を受けやすいでしょうか?もしそうなら、どう対処できるでしょうか?

A4. 受けやすいです。指紋と署名をそのまま捕捉して再送されれば、有効なリクエストとして通ってしまいます。対策として、タイムスタンプやノンス(一度きりの値)を署名対象に含めて有効期間を限定し、サーバー側で使用済みノンスを記録して再利用を拒否します。第26章のリクエスト識別子も二重実行防止に役立ちます。


全体を貫く3つの思想

  1. 処理ではなくリソースを中心に据える(リソース指向)。
  2. 標準メソッドで予測可能性を担保し、収まらないものだけカスタムメソッドに逃がす
  3. 危険な操作(一括削除・物理削除・認証なしリクエストなど)には必ず安全弁(検証・論理削除・冪等性・署名)を用意する

関連記事