読書メモ。ずっと積読状態にあった本。
- Web API - The Good Parts
- https://www.oreilly.co.jp/books/9784873116860/
Web API の設計、開発、運用についての解説書。API は設計次第で使いづらいものになってしまうだけでなく公開後の保守運用も難しくなってしまいます。そのため API を美しく設計することがとても重要です。本書では「設計の美しい API は、使いやすい、変更しやすい、頑強である、恥ずかしくない」という考えのもと、API をどのように設計し運用すればより効果的なのか、ありがちな罠や落とし穴を避けるにはどういう点に気をつけなければいけないのかを明らかにします。
めも #
エンドポイントの設計 #
HTTP メソッド #
- GET:リソースの取得
- POST:リソースの新規登録
- PUT:既存リソースの更新
- DELETE:リソースの削除
- PATCH:リソースの一部変更
- HEAD:リソースのメタ情報の取得
PATCH メソッドは PUT と同じく指定したリソースを更新するために利用するメソッドですが、すべてを変更するのではなく一部を変更するということを明示したメソッドです。PUT が送信したデータでもともとのリソースを置き換えるものであるのに対し、PATCH ではその一部だけを更新したい場合に使います。たとえば複数の値で構成される 1MB もあるような巨大なデータのごく一部を変更したい場合に、変更のたびに PUT でいちいち 1MB を送信していては非効率です。そこで PATCH を使えば、変更箇所だけのごく小さなデータを送るだけですむわけです。
API のエンドポイント設計 #
ユーザー関連の API
- ユーザー一覧の取得:
http://api.example.com/v1/user
- GET - ユーザーの新規登録:
http://api.example.com/v1/user
- POST - ユーザー情報の取得:
http://api.example.com/v1/user/:id
- GET - ユーザー情報の更新:
http://api.example.com/v1/user/:id
- PUT/PATCH - ユーザー情報の削除:
http://api.example.com/v1/user/:id
- DELETE
友達(ソーシャルグラフ)関連の API
- 友達一覧の取得:
http://api.example.com/v1/users/:id/friends
- GET - 友達の追加:
http://api.example.com/v1/users/:id/friends
- POST - 友達の削除:
http://api.example.com/v1/users/:id/friends/:id
- DELETE
友達の削除を行う際に考えなければならない点があります。友達の ID を指定しなければなりませんが、この ID に何を指定するかについて2種類の考え方ができるからです。
- 友達のユーザー ID
- ユーザー ID とは異なる、友達関係を表現する固有の ID
背後で利用されているであろうデータベースのテーブルが、ユーザーのテーブルと友達関係を表すテーブルの2つで構成されていると考えれば、ユーザーテーブルの ID を使うか、友達関係を表すテーブルにも ID を定義してそちらを使うか、と考えるとわかりやすいかもしれません。どちらを使うのが良いかと考えると、友達のユーザー ID をそのまま使う方が良いと筆者は考えます。サーバの内部的には友達関係のテーブルに固有の ID があったとしても、それを利用者に意識させる必要はありません。繰り返しになりますが、内部のアーキテクチャの都合を API に反映させる必要はまったくないのです。
リクエストの設計 #
検索とクエリパラメータの設計 - 取得数と取得位置のパラメータ #
たくさんあるデータの一部を取得する際にどういったパラメータで取得数と取得位置を指定するのか考えます。いわゆるページネーションと呼ばれる仕組みを実現するためのもので、SQL の SELECT 文でいえば limit と offset で指定する数値です。
いくつかのサービスを見てみると、クエリパラメータは次のようにしていることが多く、?limit=50&offset=100
、この際に、offset は 0 から数え始めるのが一般的です。
検索とクエリパラメータの設計 - 相対位置を利用する問題点と絶対位置でのデータ取得 #
offset といった相対的な取得位置でデータを取得する方法にはいくつもの問題点があります。更新頻度の高いデータにおいてデータに不整合が生じるという問題です。最初の 20 件を取得してから次の 20 件を取得する間にデータの更新が入ってしまった場合、実際に取得したい情報と取得された情報にズレが生じてしまうためです。
絶対位置指定とは、オフセットで相対位置を指定する代わりに、これまで取得した最後のデータの ID や時刻を記録しておいて「この ID より前のもの」や「この時刻よりも古いもの」といった指定を行う方法です。
自分の情報へのエイリアス #
ユーザー情報を取得する API があれば、アクセスしているユーザー自身の情報を取得することができます。しかし自分の情報を知るのにユーザー ID が必要になるのは不便な場合があります。自分の情報を取得したいタイミングというのは、他のユーザーの情報を取得する場合とは異なるタイミングで取得することも多く、いちいち自分の ID を調べてそれを埋め込んだエンドポイントを生成して … という処理になってしまうのは煩雑です。そこでよく利用されているのは me あるいは self というキーワードです。ユーザー情報を取得するエンドポイントでユーザー ID を指定する代わりにこのキーワードを指定すると現在のアクセストークンに紐づいた「自分」ユーザー情報を取得できるというものです。
またこのようにエンドポイントを設計することで、開発を行う際にどのユーザーの情報を出力するのかということは、認証情報からの取得が必要になり、必然的に他のユーザーの情報の取得とは処理が分岐します。出力されるデータも認証したユーザー本人の情報と他のユーザーの情報では、詳細な個人情報は認証したユーザー本人にしか返してはいけないはずで、情報がそれぞれの API では異なるはずですから、処理自体が分岐することで、間違えて他人の個人情報を丸見えにしてしまう、といったバグの混入を防ぐことが用意になります。
SSKDs と API デザイン #
ここまで検討してきたことはどちらかというと広く一般に公開し、多くの人に使ってもらうための API すなわち LSUDs 向けの API です。SSKDs 向けの API でもわかりやすく使いやすい API の設計は重要ですが、それよりももっと重要なことがあります。それはエンドユーザーにとってのユーザ体験です。
例えば E コマースサイトのアプリケーションを作っていると考えます。EC アプリのホーム画面には新着商品、人気商品、ログイン中のユーザ情報やカートの情報が出ています。もし API 設計を一般的な定石に従って行ったとすると、クライアントアプリケーションはこの画面を表示するのに何度も異なる API にアクセスしなければなりません。
したがって、とにかくホーム画面で表示する情報を1つに詰め込んだホーム画面表示用 API を作成し、それに1回アクセスするだけですべての情報が取得できるほうが利便性が高いのです。こうしたケースでは、必要とされる API の設計は、必ずしも汎用的という観点からは美しい必要はありません。「1スクリーン1 API コール、1セーブ1 API コール」という言葉があります。これはひとつの画面を表示するためにコールするのが1つの API ですむようにそれに合わせた API を用意し、データをサーバに保存する場合にも1回のコールですむようにそれ向けの API を用意するのがよい、ということです。
利便性だけでなく、データの一部だけが表示されてしまう状態や、保存の際に一部のデータだけが保存されて整合性がたもたれなくなってしまうといった問題も避けられます。
レスポンスデータの設計 #
配列の続きがあるかをどう返すべきか #
ページネーションのリクエスト結果を帰す場合に必要なのが、今取得したデータには続きがあるのか、という情報です。つまり先頭から 20 件取得した際に、そのデータが 20 件しかないのか、100 件くらいあってそのうちの 20 件なのか、ということを知る必要があります。それがなければクライアントが続きの読み込みが必要であるかどうかを知ることができず、例えば「次の 20 件」といったリンクを表示することができないからです。
サーバ側の実装を考えれば、たとえば 20 件のデータのリクエスト結果を返すために最大 21 件の取得を行ってみて、実際に 21 件取得できれば少なくとも1件の続きがあるとみなして 20 件のデータとともに「続きがあるよ」という情報をかえしてあげてもよいかもしれません。たとえば hasNext といった名前で結果に含めてあげればよいわけです。なお次があるのかという情報を返すだけでなく、次のページの URI や、次のページの取得に必要なパラメータを返すというパターンもあります。
設計変更しやすい API #
オーケストレーション層 #
LSUDs 向け、すなわち一般に公開して広くたくさんのひとに使ってもらう API の場合、どうしてもすべてのニーズを満たすことはできないので、ニーズによってはたとえば1つのアクションを行うのに複数の API にアクセスしなければならなかったり、不要なデータも受け取らなければならないこともよくあります。一方で SSKDs 向け、つまり利用者が限定されている API はユースケースに合わせた API を提供することができますが、その使い方が1つではなくいくつにも別れてしまっている場合、それぞれにあわせて API を用意したり、維持することは大変になってくるかもしれません。
LSUDs や SSKDs といった言葉を紹介した記事である「The future of API design: The orchestration layer(https://thenextweb.com/news/future-api-design-orchestration-layer)」は、まさにそういった状況に対応するために Netflix 社が構築した仕組みについての記事です。
Netflix 社では、さまざまなデバイスの機能やリリースサイクルに対応するために少しずつ違った API を提供する必要がありました。そこで Netflix 社ではサーバ側の汎用的な API とクライアントの間に、オーケストレーション層とよばれるものを挟むことで対応しています。オーケストレーション層を作成するのはクライアント側のエンジニアです。自分たちのデバイス機能やリリースサイクルに合わせて各クライアントのユースケースに最適化する層を作成します。
実際には大きな開発チームを抱えていないとここまでしっかりしたものを作る必要はないかもしれませんが、こうしたオーケストレーション層を置くことで、修正を容易にしたり、複数環境をサポートするのが容易になるという点は参考になるでしょう。
堅牢な API を作る #
悪意のある操作の対策 #
クライアントから送られてきた情報を信用せず、サーバ側でも整合性をチェックする必要があります。クライアントは次のような不正な操作を試みるかもしれません。
- パラメータの改ざんによって商品が無償で購入できてしまう
- リクエストの再送信によって商品が二重に購入できてしまう
大量アクセスへの対策 #
ネットワーク上に公開されているサービスは、外部から大量のアクセスを受けるというリスクに常に晒されています。Dos 攻撃を受けると、大量のアクセスを行ったアクセス元だけでなく、誰もがまったくサーバに接続できない状態となります。
一度に大量のアクセスがやってきてしまう問題を解決するための最も現実的な方法は、ユーザーごとのアクセス数を制限することです。つまり単位時間あたりの最大回数(レートリミット)を決め、それ以上のアクセスがあった場合にエラーを返すようにします。たとえば1分間に 60 回をアクセスの上限とした場合、61 回以上のアクセスがあった場合はエラーを返し、また1分が経過したらアクセスができるようになるといった具合です。
その際には、次のことを考慮する必要があります。
- 何を使ってユーザーを識別するのか
- レートリミットをいくつにするか
- リミットのリセットをどのタイミングで行うか
- すべてのエンドポイントまとめて回数上限をかけるのか、個々のエンドポイントで別々の上限を管理するのか