「なんとなく」でやらないための私的Web API設計ノウハウ
はじめに
エンジニアになって数ヶ月、コーディングだけでなく、基本設計や詳細設計など幅広く携わっています。
当たり前ですが、仕事で扱うWebサービスというのは、事前学習中に作ったポートフォリオとはデータベースの膨大さもドメイン知識の複雑さも桁違いです。
特にWeb APIの設計は大規模サービスの方向性を決める重要な指針であり、根拠に基づいたアプローチが必要だと感じています。
そこで、「なんとなく設計しているけど、これで本当にいいのかな...」という不安や疑問を解決するため、自分なりのWeb API設計ノウハウをまとめてみようと思います!
前提知識
本題の前に、今回考えていくWeb API、そしてWeb APIを語る上でよく聞く"RESTの原則"について、簡単にまとめます。
Web APIとは
まずAPIとは"Application Programming Interface"...ソフトウェア同士の外部インターフェイスを指します。これは、「機能はどんなものか理解しているけど、内部動作には詳しくない(詳しくなくても良い)」機能のセットを、外部から呼び出すための仕様です。
そしてWeb APIとは、HTTPプロトコルを使用して、ネットワークを介して呼び出されるAPIを指しています。
まとめると、特定のソフトウェアが別のソフトウェアのURIにアクセスすることで、ソフトウェア同士が情報の取得や変更を行えるというWebシステムの仕組みを指します。
RESTの原則とは
RESTとは"REpresentational State Transfer"の略で、Webサービスの設計モデルのひとつです。
RESTの原則の解釈と特徴は以下のようになっております。
原則 | 詳細 |
---|---|
ステートレス 各リクエストの独立性が保たれていること |
ユーザーからのリクエストを処理する際に、前回のやりとり結果の影響を受けない |
統一インターフェイス あらかじめ定義された方法(WebであればHTTPプロトコル)で情報がやりとりされること |
GET/POST/PATCH/PUT/DELETEといったHTTPリクエストを用いる データの形式はJSON/XMLを用いる |
アドレス可読性 全てのやりとりする情報が一意なURIで構成されていること |
IDなど、リソースを一意に特定する識別子をURIに含む |
接続性 1つのリンクから別の情報へ接続できること |
情報内部に、別の情報へ接続可能なハイパーリンクを含めることができる |
そして、RESTの原則に則って構築されたWeb APIは「RESTful API」と呼ばれており、誰もが見て分かるリソース名(名詞)とその操作方法を用いて設計されている、というのが特徴です。
本記事の目的
本記事では、RESTの原則を前提とし、考え方の尊重はするものの
- 仕様が明確に存在する場合はそれに従う
- 仕様が曖昧な場合はデファクトスタンダードに従う
こととして、ある程度の根拠に基づいたWeb API設計ができるようになるを目的としています。
そのため、厳密なRESTの原則に従ったノウハウ(例えば「URIに動詞を使用すべきではない」など)から若干逸れたところもありますので、ご了承ください。
エンドポイント・リクエストについて
基本
APIで公開する機能が決まったら、まずはエンドポイント(APIにアクセスするためのURI)を考えていきます。
良いエンドポイントの設計とは、書籍「Web API: The Good Parts」によれば
覚えやすく、どんな機能を持つ URI なのかがひと目でわかる
ものである、とのことです。
まずは、良いエンドポイント設計にあたり、まず押さえておきたい基本を見ていきます。
最低限の単語で構成する
https://api.example.com/service/api/users
上記のエンドポイントはapi
という単語が繰り返し使用されていたり、言わなくても分かるであろうservice
という単語が使われていますが、これらを省いて
https://api.example.com/users
としても十分意味が通じます。
また、「ユーザーを一覧表示する」際のエンドポイントを
https://api.example.com/users/list
とするケースが世に公開されているエンドポイントがたまに見受けられますが、list
という単語を使用しなくても一覧であることは通じるため、省略可能と言えます。
システム内部の状況が推測できないようにする
php
などの言語名、つまりシステムのアーキテクチャに関する単語は必要なく、盛り込むと逆に攻撃対象となってしまいます。
単語は省略せずに書く
user
をu
と省略するなど、ぱっと見て理解できない省略形は避けるべきです。
小文字を用いる
大文字小文字を混在させると、打ち間違いが多発してしまいます。
関連エンドポイントが容易に想定できる
ユーザーを特定するエンドポイントを
https://api.example.com/users/123
と設計した場合、
「users/456とするとuserIdが456のユーザーを特定できるんだな」と、URIから他のURIを容易に推測することができます。
このような想定・改造がしやすいエンドポイントは、万が一開発者がAPIドキュメントを十分に確認せず開発を進めたとしても、それにより発生するバグのリスクを軽減することができます。
同じような観点から、
特定ユーザーを表示するエンドポイントではhttps://api.example.com/users?id=123
とクエリパラメータを用いる一方、
特定ユーザーにメッセージを送るエンドポイントではhttps://api.example.com/users/123/message
とパスパラメータを用いるなど、統一性なく使用することは、避けるべきです。
ちなみにこの例で指定しているIDは、一意のリソースを表すのに必要な情報であるため、パスパラメータを指定する方が望ましいです。詳しくは後述しています。
リソース表記には複数形を利用する
テーブル名が複数形を用いているように、複数形は「集合」であることを表します。
エンドポイントに複数形を用いることにより、Webでリソースの「集合」を扱っている、という意思を明確にすることができます。
迷ったらアンダーバーではなくハイフン
単語を繋ぐ際に_(アンダーバー)を使うべきか-(ハイフン)を使うべきかは、公開されているAPIを見てもバラバラで、どちらが正しいかは一概に言えません。
ただ、リンクアドレスに下線が引かれた際にアンダーバーだと見えずらいので、ハイフンを推奨するという声が多いようです。
エンドポイントとHTTPメソッド
エンドポイントでリソースを表したら、その操作方法はHTTPメソッドによって明らかにします。
操作したい内容によって、適切なメソッドを選択することが重要です。
メソッド名 | 用途 |
---|---|
GET | データを取得する |
POST | 送信されたデータを追加する |
PUT | 既存データを修正する 指定のデータを、送られてきたデータにまるまる置き換える |
PATCH | 既存データを修正する 指定のデータを、送られてきた項目だけ部分的に置き換える |
DELETE | データを削除する |
具体例
では具体的に、簡単なSNSサービスを例に挙げて、具体的なエンドポイント設計を考えてみます。
テーブル構造はこんなイメージです。
単一リソースを用いる機能
まずは、単一リソース(ここでは登録ユーザー)を操作する場合です。この場合はほぼ決まった形で設計を行うことができます。
特定のユーザーについて対応する必要がある場合は、ユーザー固有の識別子であるIDをパスパラメータに用いています。
(例)登録ユーザーの一覧取得
GET: https://api.example.com/v1/users
(例)サービス退会(特定のユーザーの削除)
DELETE: https://api.example.com/v1/users/{id}
複数テーブルにまたがる機能
登録ユーザーを表すテーブルの他に、フォロワーを表すテーブルを操作する場合については、エンドポイントをusers/{userId}/followersという形を基本とすることで、何を表すエンドポイントなのかが明確になります。
(例)特定のユーザーの友達一覧を表示する
GET: https://api.example.com/v1/users/{id}/followers
また、特定のユーザーの、特定のフォロワーについて操作を行う場合、followers
の後のID指定は
①フォローしているのユーザーIDを指定するのか
②フォロー・フォロワー関係を示すIDを指定するのか
悩ましいところではありますが、シンプルさや改造しやすさを考えると、②のように新たな概念のIDを追加するよりは、①のようにユーザーIDで統一した方が良さそうです。
(例)特定のユーザーの特定の友達を削除する(友達から外す)
DELETE: https://api.example.com/v1/users/{id}/followers/{id}
パラメータの使い分け
特定のリソースを指定する方法には、パスパラメータを指定する方法のほか、クエリパラメータで指定する、という方法もあります。
この2つの使い分けについては、
- 一意のリソースを表すのに必要な情報か(一意に特定できない場合はクエリパラメータを用いる)
- 省略可能であるか(可能な場合はクエリパラメータを用いる)
が主な指標となります。
例えば、特定のユーザーを「年齢」で絞り込みしたい場合のAPIは、年齢を表す数字ではユーザー情報を一意に特定できないため、
GET: https://api.example.com/v1/users?age=30
というようにクエリパラメータを用いるべきです。
またユーザー一覧を、1ページあたりの情報数limit
を指定して取得するといった場合でも、情報数の指定しなくても一覧表示は可能であるため
GET: https://api.example.com/v1/users?limit=10
というようにクエリパラメータを用いるべきと言えます。
検索APIについて
エンドポイントはリソースを表すもの、という前提に基づけば、search
という動詞をエンドポイントに含めることはやや邪道に思われるかもしれません。
ただし、このエンドポイントは検索ありきです、という意思表示をしたい場合は、search
という単語が有効に働きます。
例として、SNSサービスで友達の投稿を一覧取得・検索するAPIなどが挙げられます。全ユーザーの投稿一覧の量を取得するのは、量が膨大すぎて検索をかけないと意味を為しません。
(例)近況報告を一覧表示する
GET: https://api.example.com/v1/posts/search
ホーム画面のAPIについて
ECサイトのホーム画面を見ると、「ログインユーザー情報」「新着アイテム」「レコメンドアイテム」「人気アイテム」などが1つのページにまとまっていることが多いです。
こういったケースでは、それぞれの情報を取得するAPIを何度も作成する必要はなく、必要な情報を全て取得するホーム画面APIを用いることで対応した方が効率的です。
また後述する「何度もAPIにアクセスしなければならない」という状況を避けることもできます。
レスポンスについて
エンドポイントやリクエスト内容について考慮できたら、次にWeb APIにアクセスした結果返却されるデータ...レスポンスについて見ていきます。
必要な情報はなるべく1つのAPIで取得する
エンドポイントの注意点として、「システム内部の状況が推測できないようにする」と先述しましたが、同様なことがレスポンスにおいても言えます。
こちらも書籍「Web API: The Good Parts」から引用しますが、レスポンスを返す上で重要なことは
API は内部で持っている DB のテーブル構造をそのまま反映したものである必要はまったくありません。
とのことです。
前章で挙げた、特定のユーザーのフォロワー一覧を表示する
GET: https://api.example.com/v1/users/{id}/followers
というAPIのレスポンスを考えてみます。
もし内部で持っているDBのテーブル構造をそのまま反映するとしたら、フォロー・フォロワー関係を表すテーブルにはユーザーIDしか含んでいないので、フォロワーのユーザーIDを配列で返却するだけの方が、シンプルにサーバーを構築できそうです。
しかし、このAPIを使って画面表示したい内容は、フォロワーの名前やステータスなど、IDだけではないはずです。
上記のようなAPIを作成した場合、フォロワーのユーザーIDに基づいて、ユーザー情報を取得するためのAPIに再度アクセスしなければ、必要なデータを取ることができません。
こういった、ひとつの作業を行うのに何度もアクセスを必要とするようなAPIは、開発者からも利用者からも面倒と思われる仕様になってしまいます。
レスポンスはテーブル構造に囚われない、ユースケースを考慮したデータを返すことが重要です。
階層の必要性
次に、ユーザー同士で商品の売買ができるサービスをイメージして、商品の名前と売買に関わったユーザーの一覧を取得するAPIのレスポンスデータ構造について考えてみます。
以下のように階層的に表す方法もあれば⇩
"id": 1,
"name": "smartphone",
"seller": {
"id": 2,
"name": "Taro"
},
"buyer": {
"id": 3,
"name": "Hanako"
}
フラットに表す方法もあります⇩
"id": 1,
"name": "smartphone",
"seller_id": 2,
"seller_name": "Taro",
"buyer_id": 3,
"buyer_name": "Hanako"
このケースでは"seller(商品を売った人)"と"buyer(商品を買った人)"という、同じ「ユーザー」というリソースを表しています。
同じリソース同士でも別の意味を持たせたい場合は、階層的にまとめた方が情報が整理できて分かりやすいです。
配列データの扱い方
次に、特定ユーザーのフォロワーの情報を一覧で返すAPIのレスポンスデータをイメージします。
以下のように配列をそのまま返す場合と⇩
[
{
"id": 1,
"name": "Taro",
"age": 25,
...
},
{
"id": 2,
"name": "Jiro",
"age": 30,
...
},
...
]
レスポンス全体をオブジェクトに包んで返す場合とがあります⇩
{
"followers": [
{
"id": 1,
"name": "Taro",
"age": 25,
...
},
{
"id": 2,
"name": "jiro",
"age": 30,
...
},
...
]
}
非常に些細な観点ではありますが、配列をそのまま返す場合、JSONインジェクションのリスクが生じ、脆弱性が高まるリスクがあるため、オブジェクトで包むのが基本です。
JSONインジェクションは簡単に言うと、悪意のあるユーザーが<script>
タグを利用してJSONデータに不正なコードを挿入し、サーバーに対してセキュリティ上のリスクを引き起こす攻撃手法で、読み込んだJSONファイルがJavaScriptの文法になっている場合に発生します。
JavaScriptの文法において、ルートに存在する{}
(ブロック)の中に[]
配列があるのは問題ないですが、再度{}
(ブロック)が入ると構文エラーとなります。
構文エラーを発生させブラウザが読み込むことができないデータ構造にすることで、JSONインジェクションを避けることができます。
データ項目名について
レスポンスデータの項目についても、エンドポイントの命名と似たような注意点がいくつかあります。
- なるべく単語数は少なくする工夫をする。 エンドポイント内に含まれている単語は省略する、時間は’at’で表現する、など
- 複数単語の連結方法は、スネークケースorキャメルケースを統一する。
- 省略形は使わない
- 単数形と複数形を意識する。 前者は1つのデータを表すとき、後者は配列を表すときに用いる。
ステータスコードを返す
レスポンスデータには、取得してきたデータのみならず、
- ステータスコード
- 成功or失敗、失敗した場合はその詳細
を返却をレスポンスボディに盛り込む必要があります。
特に何らかの理由で処理に失敗した場合、「エラーが発生した」旨だけでなく、どんな理由で(パラメータが異なっていたのか、アクセス許可がないのか、など)エラーが発生したのかを適切に返却することが、丁寧なWeb APIにおいては必須といえます。
言わずもがなですが、正しいステータスコードを使用するのが条件です。
ステータスコードが200にも関わらず、詳細に「アクセスが拒否されました」と書かれているAPIは、整合性が取れていません。
ステータスコード | 内容 |
---|---|
200 OK | リクエスト成功 |
201 Created | リクエストが成功し、新しいリソースが作られた |
204 No Content | コンテンツなし |
400 Bad Request | リクエストが正しくない |
401 Unauthorized | 認証が必要 |
403 Forbidden | アクセスが禁止されている |
404 Not Found | 指定したリソースが見つからない |
500 Internal Server Error | サーバ側でエラーが発生した |
503 Service Unavailable | サーバが一時的に停止している |
ただし、中には意図的に正しくないステータスコードを返却する方が良い場合もあります。
SNSサービスで、特定のユーザーからのメッセージやコメント、投稿閲覧を拒否するブロック機能をイメージしてください。
ブロックされているユーザーが、ブロックしているユーザーのページを見に行った際、ステータスコードが"403 Forbidden"(アクセスが許可されていません)と表示された場合、ブロックされているという事実が分かってしまい、ユーザー同士のトラブルを招く可能性があります。
そうならないように、あえて"404 Not Found"(情報が見つからない)として、ステータスコードをずらすというケースもあるようです。
変更に強いAPIを作るには?
最後に、長期的に運用できるWeb APIについて考えてみたいと思います。
Web APIの使用や公開を続けているうちに、当初とは別の使い方をしたり、APIに新たな機能要件を追加したり、はたまた公開を停止したり...ということもあり得ます。
Web APIがその役目を終えた場合については、古いAPIは変更せず残しておき、新しいAPIを用意する、というのがベストです。
あらかじめパスの共通部分にv1
, v2
といったバージョンを盛り込んでおくのが分かりやすいと思います。
(例)https://api.example.com/v1/users
そして古いWeb APIについては、いきなり終了するのではなく、ユーザーに公開終了時期を十分にアナウンスし、公開終了したらステータスコード410(Gone)を返却、そしてより新しいバージョンを使うように促すことで、ユーザーの困惑を少しでも減らす、という工夫が重要となってきます。
おわりに
Web API設計については、完璧なマニュアルが存在しておらず、明確な答えというものがありません。
これまで挙げたノウハウについても、あくまで判断に迷った時のアイデアに過ぎず、サービス要件やプロジェクトの状況によっては適さない場合もあります。
もし、既にプロジェクト内で統一されているルールが存在するならば、それをプロジェクト内で徹底することが重要だと思います。
それを踏まえて、新たなルールを追加する際や、判断に迷った際に、本記事が少しでも参考になれば幸いです!
Discussion