API Design

はじめに

改めて自分の中でAPI設計について整理するために、Googleが公開しているAPI設計ガイドを読みました。
cloud.google.com
基本的なリソースやパス設計の考え方はRails Wayに馴染みのあるものですが、一部自分の新たな気付きになったものを中心にピックアップして、自分用にまとめました。
なお、この記事内の記載している例については、ガイド内で紹介されている内容を引用しています。
また、RESTに限らずgRPCなどのケースであっても適用可能なガイドになっています。

用語

コレクション
リソースに対し、コレクションは同じタイプのサブリソースのリストを含む特殊なリソース。

標準メソッド
GoogleAPI設計では、リソースに対する操作は基本的に、List/Get/Create/Update/Delete を提供します。
多数のリソース+少数のメソッドでAPI設計をするのがベターとされています。

カスタムメソッド
標準メソッドだけでは表現が困難な操作があります。
例えば、「削除操作の取り消し」や「インスタンスの再起動」などがあります。
標準メソッドの動詞(List|Get|Create|Update|Delete)だけでは表現できない操作を対象に定義されます。

API設計について

標準メソッドの場合

List

List を使用する場合は、コレクション名+0個以上のパラメータを使用します。
また、List操作はHTTP GETメソッドを使用する必要があり、コレクション名はパスにマッピングする(本文に含めない)ことが推奨されます。
例)

  • GET shelves (コレクション名: shelves)
  • GET shelves/shelf1/books (コレクション名: books)

Listを使用するケースとしては、検索が代表的です。
ただし、簡易的な検索(フィルタリングなど)ではなく、範囲の広い検索においては、カスタムメソッドsearchを使用することが推奨されます。
また、レスポンス本文には任意のメタデータを含めることが推奨されます。
例えば、ページングを行うための page_sizenext_token を含めます。

Get

Get を使用する場合は、リソース名+0個以上のパラメータを使用します。
また、HTTP GETメソッドを使用する必要がありリソース名はパスにマッピングすることが推奨されます。
例)

  • GET shelves/shelf1 (リソース名: shelf1)
  • GET shelves/shelf1/books/book2 (リソース名: book2)

返却されるリソースは本文全体にマッピングされます。
=> Getのレスポンス専用の型を用意してならず、リソース全体を本文として返します。
CreateやUpdateでも同様です。

Create

Create を使用する場合は、親リソース名+リソース+0個以上のパラメータを使用します。
また、HTTP POSTメソッドを使用する必要があり、リクエストメッセージには親リソースID parent を含めることが推奨されます。
例)

{
  "parent": "shelves/shelf1",
  "book": {
    "name": "book name"
  }
}

リソースに関連しないその他リクエストメッセージは、すべてクエリパラメータにマッピングされるべきです。
Getの場合と同様、作成したリソースは、レスポンス本文全体にマッピングされるべきです。

Update

Update を使用する場合は、リソース名+0個以上のパラメータを使用します。
リソース名や親リソース名を除き、変更可能なリソースのプロパティは基本的に更新可能にする必要があります。
また、リソース名の変更や親の移動を行う場合は、カスタムメソッドを使用することが推奨されます。

レスポンスに関しては、更新されたリソース自体であることが必須となります。
また、部分的な更新を行えるように、FiledMaskでPATCHを使用する必要もあります。

例)
PATCH shelves/shelf1/books/book2

{
  "book": {
    "name": "updated book name"
  },
  "update_mask": "book.name"
}

リソースに関連しない残りのメッセージフィールドは、すべてクエリパラメータにマッピングされるべきです。

Delete

Delete を使用する場合は、リソース名+0個以上のパラメータを使用します。
また、HTTP DELETEメソッドを使用する必要があり、リソース名はパスにマッピングすることが推奨されます。
残りのメッセージフィールドは、すべてクエリパラメータにマッピングされるべきです。
例) DELETE shelves/shelf1/books/book2

Delete は、指定したリソースを即時削除するか、削除をスケジュールします。
即時削除の場合、google.protobuf.Empty(空の本文)を返すことが推奨される。
削除済みとしてマークするだけの場合、更新されたリソースを返すことが推奨される。

カスタムメソッドの場合

カスタムメソッドを使用する場合、基本的には柔軟性を持つ(bodyを使用できる) HTTP POST メソッドが推奨されます。
一方、PATCH の使用は推奨されません。
また、カスタムメソッドが関連するリソースやコレクションのリソース名は、パスにマッピングされることが推奨されます。
カスタムメソッドが有効な代表的なケースは以下のように紹介されています。

メソッド名 動詞 HTTP Method
キャンセル :cancel POST
batchGet :batchGet GET
移動 :move POST
検索 :search GET
削除の取り消し :undelete POST

(削除の取り消しについて、削除済みのリソースの推奨保持期間は30日。)

GETを使用するケースでは、冪等であり副作用がないことが前提となります。

カスタム動詞を使用する場合は、動詞とリソース名を / ではなく : で区切ります(/を含まないことで、任意のパスで対応できるため)。
例) shelves/shelf1:undelete

カスタム動詞は接尾辞として付与することが必須です。

命名について

リソースID

リソース指向APIでは、リソースは名前付きエンティティであり、リソース名はその識別子となります。
各リソースには、一意性を表すためのリソース名が必要であり、以下で構成されます。

  1. リソース自体のID
  2. すべての親リソースのID
  3. そのAPIサービス名

例)

  • "//library.googleapis.com/shelves/shelf1/books/book2" (完全なリソース名)
  • "shelves/shelf1/books/book2" (相対的なリソース名)

コレクションIDの設計

コレクション名の設計については、以下を満たすように設計します。

  1. 簡素で明確な英単語を使用する
  2. キャメルケースで使用する
  3. コレクションIDでは、非常に一般的な用語は使用しないか、修飾して使用する(values -> rowValues)

以下の単語は、修飾して使用することが推奨されます。

  • elements
  • entries
  • instances
  • items
  • objects
  • resources
  • types
  • values

リソース定義

リソースを定義する場合、最初のフィールドはリソース名の文字列フィールドにする必要があり、フィールド名にはnameという用語を用います。
これは、nameが非常に広い意味を持つ用語であり、開発者によって解釈が異なる可能性が高いため、予約語として使用します(その他のフィールド名では、titleなどより明確な用語を使用することが強いられる)。

一般的な設計パターン

空レスポンス

Deleteでは、ソフト削除を除いて基本的にgoogle.protobuf.Empty(空の本文)を返す必要がある。
一方、ソフト削除の場合は状態の変更がわかるように、更新後のリソースを返す必要がある。

区間表現

半開区間を使用する必要がある。
また、命名には start_xxxend_xxx といった規則を用いる。
例) [start_date, end_date) / [start_time, end_time)

型は Timestamp

長時間実行オペレーション

実行に長時間かかるかかるAPIメソッドは、長時間実行オペレーションのリソースをクライアントに返すように設計できます。
Operationインターフェースを使用し、長時間実行APIごとにインターフェースが独自に定義されないようにする必要があります。

サブコレクションのList

shelfとサブコレクションであるbookに対し、すべてのshelfに属するbookを検索する(親のshelfに対する制約がない)場合、以下のようなREST APIを使用します。 例) GET https://library.googleapis.com/v1/shelves/-/books?filter=xxx
親コレクションに対してワイルドカードとして-を使用します。

サブコレクションからの一意なリソース取得

取得したいbookリソースが、親であるshelfリソースのIDを知らなくても取得できるようにしておくと便利です。
その場合、以下のようなREST APIを使用できます。
例) GET https://library.googleapis.com/v1/shelves/-/books/{id}

サブコレクションのListと同様、ワイルドカードとして-を使用します。

並び替え順序

リクエストに order_by を含め、SQL構文に従いカンマ区切りで指定します。
foo, bar desc

リクエストの重複

基本的に冪等なAPIが好まれますが、簡単には冪等にできないケースもあります(リソースの作成やデータベーストランザクションなど)。
このような場合、リクエストにUUIDなど一意な値を持たせることが推奨されます。
サーバーはその値を用いて重複を検知し、リクエストが1度だけ実行されるようにします。

列挙型のデフォルト値

列挙型エントリは0から開始することが必須となります。
明示的に指定されていない場合、このエントリが使用されることが推奨されます。
一般的なデフォルト動作がない場合、列挙型の0ENUM_TYPE_UNSPECIFIEDという名前にすることが推奨されます。
また、これが使用された場合は、INVALID_ARGUMENTで拒否することが推奨されます。

部分レスポンス

レスポンスメッセージ内の特定のサブセットのみが必要なケースでは、暗黙的なシステムクエリパラメータ$fieldを使用します。
例) GET https://library.googleapis.com/v1/shelves?$fields=name

これは、FieldMaskのJSON表現として使用できます。

シングルトンリソース

例えば、ユーザーリソースに対してユーザー固有の設定はシングルトンリソースとして存在します。
シングルトンリソースは、標準のCreate/Deleteが省略されることが必須です。
親が作成または削除されると、暗黙的に作成、削除されます。
リソースにアクセスするためには、Get/Updateかカスタムメソッドを用いてアクセスしなければなりません。

データ保持

誤ってデータを削除してしまうことは多々あり、ビジネス的ダメージを軽減するため、データの削除ポリシーとして一定期間データを保持することが推奨されます。
データ保持の推奨期間は以下のように記載されています。