WebAPIを設計するときに考えること
背景
WebAPIを設計してきて、最近ある程度考え方・注意点がまとまってきたので整理も兼ねてメモっときます。観点
同じ事柄を表すリソース構造には一貫性があること
例えば「人」を表す際に、叩くAPIによってリソース構造を変えてはいけない(単体での情報取得APIと一覧取得のAPI、検索APIなど)。クライアント*1が扱うリソース構造を共通化させてあげる。それを実現するには、WebAPI(HTTPメソッドとかパス)を考える前にリソース自体の構造をまず整理したほうがよい。
リソースには何かしらの標準があったりするので参考にできるならしたほうがいい。IANA — Protocol Registriesなど。
- 例:"人リソース"で構造が違うダメな例
叩くAPIによって { "id" : 123, "name" : "なみひら" } と { "id" : "123", "familyName" : "なみひら" } とか構造が異なる。
表示を意識したパラメータ・データ構造を避けること
表示の要件を入れると表示の仕様変更に強く影響を受けてしまうので、あくまでデータを返すことで変更耐性を持たせる。- 例:表示とデータの違い
- 「10」が扱うデータであり、「0010」は表示を含んだ情報
表示順序などはアプリ側の要件次第(表示の仕方次第)で多数に存在しうるので、"sort(orderBy)"クエリなどでを設けてクライアントの要望で意図的に指定された通りに返す。
クライアントがハンドリングしそうな値は文字列ではなく数値を使う
文字列だと「比較(containやstartWith、endWith、equelsなど)」で間違いを引き起こしやすくなる、またクライアントによっては処理上の禁則文字があったりする(ミスリードしやすい*2)。特に文字列に意味を持たせると("id"といいつつ"アプリ名"っぽい表現を返すとか。"URL"っぽい値を返すとか。)、クライアントが勝手にその値を他の用途に利用しだす可能性があるので避ける(API互換に問題を起こす。)。もし文字列を利用する場合でも、複雑な正規表現などもできるだけ使わせない(=値から値を切り出したくなるような形式は避ける。値の中に別の意味を持たせない)。
数値にするとAPI上の可読性が低下するかもしれないが、人が認識できるかどうはあまり考慮しなくていい。人間には仕様書を読んでもらえばよい。
ダメな例:
×「/」とかクライントに不具合を起こさせそう。 ×URLではないのにURLっぽくみえる値 { "id" : "http://example.com/foo.txt", "name" : "foo.txt" } ○ハンドリングしそうな値は数字で表現 { "id" : 48372482743274982749, "name" : "foo.txt" }
1つの属性が複数の値をもつことを考慮する
値が完全に排他な値をもつかはなかなか判断しかねるので、複数の値を持つことを考慮しておく(配列で定義したほうが無難)。これを考えると配列の要素がかなり多くなる(;´Д`)
例:所属組織は1つの場合が多いかもしれないが、複数の値をもつことも想定される(開発G かつ マーケ兼務など)。Number型 organization -> Array[Number]型 organizationsなど。
{ "name" : "なみひら", "organization" : 10 } よりかは { "name" : "なみひら", "organizations" : [ 10 ] } としておいたほうが変更に強そう。
多数の属性の値を組み合せて利用するような値は設定しない
値同士の関連が発生してしまい、一方の値を形式を変更しにくくなるため、値同士の組合せを助長するような値の設定は避ける。特にクライアントの利用の仕方は提供側の知らないところで残るものなので、一回ミスリードさせると厄介。
ダメな例: クライアントに次のアクションとして"folder_url"と"name"を組合せてデータを取ることを助長する("http://example.com/hoge/foo.docx")。 "name"を拡張子なしのファイル名に変更したくでもできない("foo.docx" -> "foo"とか)。 { "folder_url" : "http://example.com/hoge", "files" : [ { "name" : "foo.docx" } ] } 以下のようにする。 クライアントは"url"のみで次のアクションをしていることが想定できるため、nameの形式を柔軟に変更できる。("foo.docx" -> "foo"とか) { "folder_url" : "http://example.com/hoge", "files" : [ { "url" : "http://example.com/hoge/foo.docx", "name" : "foo.docx" } ] }
コレクションリソース(一覧の意)を返すときは、数の増加を想定しページングを設ける
トップレベルで配列を返すよりも、1段ネストさせたほうが融通がきく。[ { "id" : 1, "name" : "なみひら" }, { "id" : 2, "name" : "たなか" } ] よりも "results" : [ { "id" : 1, "name" : "なみひら" }, { "id" : 2, "name" : "たなか" } ] とかしたほうがその他の情報("総数"の情報とか)が必要になった際にあとから入れやすい。 "totalresults" : 10, "nextPageToken" : 3874832748762137213712098, "request" : { "sort" : "id" }, "results" : [ { "id" : 1, "name" : "なみひら" }, { "id" : 2, "name" : "たなか" } ]
ページングをする際のデフォルトの動きは、データを出来る限り全て返せるように振る舞う
一覧取得の際は、クライアントにクライアントの選択で自由に値を返すようにしてあげる。全てのデータを取りたいクライアントの要望にも答えるため、全ての値を取得できるようにデフォルト設定を設ける。例えば5000個で全データを包括できる場合、「無指定だと5000個のデータを返却します」など。極力ページングの機能を使わせないようにする。データをちょろちょろ取らせない。転送する通信帯域(データサイズ)が問題になりそうな場合、返すパラメータを絞れるクエリを検討する
上記でのページングをデータ削減のために用意するのではなく、データ削減には返すパラメータで少なくして改善する。- ページング:欲しいリソースの絞り込み。データサイズを意識すべきではない仕組み。クライントにとってはデータサイズが重くても欲しいリソースは取得しないといけない。
- 返却されるパラメータ限定:リソース内のデータの絞り込み。クライントにとっては全ての属性が必要な場合は少ない。
例:人物一覧取得でIDと名前のみ取得する場合 リクエスト GET /people レスポンス { "results" : [ { "id" : 1, "name" : "なみひら" ・・・ (属性が100個) }, { "id" : 2, "name" : "たなか" (属性が100個) } ] } GET /people?filter=id,name リクエスト GET /people レスポンス { "results" : [ { "id" : 1, "name" : "なみひら" }, { "id" : 2, "name" : "たなか" } ] }
領域(ドメイン?)概念の異なる値を同じ配列に入れない。別パラメータとして設ける。
クライアントが必ずフィルター操作やマージ操作が想定されるケース。上記で挙げたソート指定やパラメータ絞り込みがわかりにくい、効きにくい、やりにくい。ちょっと似ている概念をまとめたくなる場合に発生しやすい。
ダメな例 [ { "id" : 1, "value" : "政治", "type" : "book_category" }, { "id" : 22, "value" : "経済", "type" : "book_category" }, { "id" : 9999, "value" : "新品", "type" : "condition_category" } ] 良い例 { "book_categories": [ { "id": 1, "value": "政治" }, { "id": 22, "value": "経済" } ], "condition_categories": [ { "id": 9999, "value": "新品" } ] }