2020.01.03

SPA開発におけるWeb API設計入門(エンドポイント編)


この記事では、シングルページアプリケーション開発での Web API 設計について書いていきます。

ここで言う「エンドポイント」とは、HTTP メソッドと URL の組み合わせです。また、本記事で扱うのは、いわゆる REST API と呼ばれるタイプの Web API です。最近は GraphQL が台頭してきていますが、まだ現場では REST タイプの API を扱うことがほとんどでしょう。

API 設計は大きく2つの側面があります。エンドポイント定義と、リクエストおよびレスポンスメッセージの JSON 定義です。本記事では、特にエンドポイント定義の設計について取りあげます。なぜなら、どちらかというと、エンドポイント定義のほうが、これから SPA 開発にチャレンジする方にとって、難しさがあるように感じるからです。

Web API とは

何を API にするのか

まず、そもそも何を API にすればよいのか、というお話から始めます。

Web API とは、リソースサーバがフロントエンドを通してユーザーに公開する機能群です。

つまり、1機能が1つの API になります。

反対に、以下の図のように、1画面を構成するデータをまとめて返却する API を、画面ごとに作ってしまう、または1つのボタンアクションから呼び出す API を、ボタンごとに作ってしまう設計モデルは、好ましくない例です。

良くない設計モデル

API は、シンプルな機能ごとに作られてこそ威力を発揮します。ある画面を構成するために呼び出す必要な API が複数あっても OK ですし、同じ機能のボタンであれば、同じ API を呼び出せばよいのです。

良い設計モデル

では、「機能」とは何でしょうか?

たとえば、各プロジェクトで「機能一覧」のようなドキュメントを作ることが多いと思いますが、当然プロジェクトごとに記載粒度は異なるはずですので、これは基準になりません。

また、テーブルごとに(機械的に)CRUD を切り出せばいいのかというと、結果的にそうなることもあるかもしれませんが、本格的なアプリケーションはそんなに単純な機能ばかりではないでしょう。

後述する、「リソース(何を)」と「メソッド(どうする)」で表現できる単位、と理解できればよいかと思います。

いつ API を設計しはじめるか

次に、API の設計が、全体の設計のうちどのタイミングに位置するかをお話しします。

結論から言うと、UI(画面)がある程度固まって、データベース定義の初期段階、ER 図ができたあたりから API を設計し始めることをお勧めします。

そもそも SPA には以下の図のような設計ポイントがあります。つまり、UI 設計、API 設計、データベース設計と、フロントエンド・サーバサイドそれぞれのビジネスロジックです。

設計ポイント

個人的な意見としては、設計の観点で特に重要なのが UI / API / DB の設計です。それぞれを繋ぐビジネスロジックについては、アイディアの共有という意味で文章や図にするのはいいことだと思いますが、詳細に設計すればするほどプログラムコードに近くなるので、正式なドキュメントとしての価値はコードで担保できると考えています。

さて、話を戻すと、設計の順番として UI → DB モデリング → API がオススメだと言っても、そもそも DB が UI のために存在したり、API が DB のために存在したりするわけではありません。あくまでそれぞれが、ユーザーの体験や与えたい、感じてほしい価値に従うべきです。

設計ポイント

ただ、実際の案件では、UI からユーザーの体験を設計し始めることが多いでしょう。モック的に作成した UI から着想を得て、もっとこうしたらいいのでは、などのアイディアが出てきて、だんだんと作りたいアプリが具体的に見えてきます。

設計ポイント

アプリが実現するべき機能が具体化してくると、必要なデータおよびその構造を考え始めることができます。全体のデータモデリングが、サーバサイドが持つリソース(リソースについては後述しますが、テーブル=リソースではありません)の骨格となるので、そこから API の設計に入るのがちょうどいいと思います。

UI / API / DB が相互に交換可能に設計できているのが理想でしょう。たとえば UI が Web からモバイルアプリに替わっても、または DB がオンプレの RDB から Firebase のようなサービスに替わっても、その他の設計は変更しなくて OK というように。

ただし理想は理想であって、設計はバランスです。ある程度、UI の都合に合わせて API を設計することもあっていいと思います。

少し話が広がってしまいましたが、ここから API のエンドポイントについて考えていきます。

エンドポイントの基本構造

まずはエンドポイントの基本構造についてのお話です。

以下は、HTTP リクエストの例です。

GET /articles HTTP/1.1
Host: foo.bar.com
Accept: application/json

このうち、エンドポイント設計において考えるのは、GET /articles の部分、つまり HTTP メソッドとリクエスト対象(URL)の組み合わせです。

リソースとメソッド

Web API では、URL 部分を「リソース」と見なし、HTTP メソッドと URL の組み合わせを、

「なにを(= リソース)」「どうする(= HTTP メソッド)」

という形式で捉えます。

GET /products

ここでのリソースは products(商品)で、メソッドは GET(取得)です。つまり、「商品一覧を取得する」という意味です。

リソース

リソースとは、URL で表される、API によって操作される対象のことです。

なんだか抽象的な言い方になってしまいましたが、直接的な説明や置き換えが難しい概念です。たとえばデータベースのテーブルすなわちリソースかと言うと、もちろんそうなる場合もありますが、厳密にイコールである必要はありません。

HTTP メソッドと組み合わせて、うまく機能を表現するように作っていきます。

具体的な例は後述します。

HTTP メソッド

HTTP メソッドには以下の種類があります。正確にはあといくつかありますが、設計において扱うのは以下の5種類です。

メソッド 意味
GET リソースの取得
POST リソースの追加
PATCH リソースの一部更新
PUT リソースの置き換え
DELETE リソースの削除

旧来の(SPAではない)マルチページアプリではメソッドが GET と POST しか使えませんでした。なぜなら、HTML のフォーム送信が GET と POST にしか対応できていないためです。

2種類しかなく表現力に乏しかったため、たとえば、以下のように、「どうする」の部分の一部を URL が担っていました。

POST /products/create
POST /products/update

HTML フォームを介さない非同期通信(つまり Web API)では、このような制約はなく、HTTP に定義されたメソッドをフル活用できるので、以下のようなエンドポイントを表現することが可能となりました。

POST  /products
PATCH /products

「どうする」の表現を HTTP メソッドに任せられるようになったからこそ、「何を」に専念する「リソース」という設計上の概念も可能となったと言えるでしょう。

階層構造

もう一つ、基本として押さえてほしいのは、URL は階層構造で表現されるという点です。

これは Web API に限らず、「普通の」Web サイトにも当てはまる、普遍的な特徴です。たとえば、以下の例を見てください。ありそうな企業ホームページの URL を考えてみました。

URL ページ
/ トップページ
/services 事業一覧
/services/web-development Web 開発事業紹介
/services/marketing マーケティング事業紹介
/recruit 採用トップ
/recruit/jobs 採用職種一覧
/recruit/jobs/designer デザイナー募集要項

トップページの子階層として事業一覧ページがあり、事業一覧ページのさらに子階層に各事業の紹介ページがある、といったように、URL は親子関係やツリーで表されるような階層構造で定義されます。

階層構造は、Web API においてリソースを表現する際にも重要な考え方になります。

リソースの表現

ここからは、より具体的な設計のポイントに話を進めます。

まずは「リソース」の表現方法についてです。

階層構造を利用したリソース表現

先ほども述べた通り、リソースも URL の特徴に倣って階層構造で表現しましょう。

以下の例において、123 の部分は本を一意に特定する ID だと思ってください。

URL 意味
/books 蔵書一覧
/books/123 特定の一冊
/books/123/authors ある本の著者
/books/123/reviews ある本のレビュー

このように、操作の対象=リソースも階層構造を用いて表現します。HTTP メソッドと組み合わせると、たとえば以下のようなエンドポイントができあがります。

レビューの投稿
POST /books/123/reviews

もうひとつ階層化の例です。

URL 意味
/bands バンド一覧
/bands/the-beatles ビートルズ
/bands/the-beatles/albums ビートルズのアルバム
/bands/the-beatles/albums/abbey-road 『アビィ・ロード』
/bands/the-beatles/albums/abbey-road/songs 『アビィ・ロード』収録曲
/bands/the-beatles/albums/abbey-road/songs/something 『サムシング』

階層が増えて、多少長く感じても問題はありません。そのような感覚にはほとんど根拠がないでしょうし、階層が崩れて意味が(一部の開発者にしか)分からなくなってしまうことのほうが問題です。

コトをリソースとして表現する

「モノ」だけでなく、「コト」もリソースとして表現しなければなりません。

「商品」「コメント」「書籍」など、明らかにモノであるリソースは考えやすいでしょう。機能としては、「商品を登録する」「コメントを編集する」などですね。しかし、「商品をお気に入りに入れる」「コメントにいいねする」「本を貸し出す」などはどうでしょうか?

たとえば、コメントにいいねする場合のリソースは以下のように考えられます。

/comments/コメントID/like

「いいね」をリソースとして捉えるわけです。HTTP メソッドと組み合わせると、以下のようなエンドポイントを作れます。

いいねする
PUT /comments/コメントID/like
いいねを取り消す
DELETE /comments/コメントID/like

以下は、SNS アプリで、あるユーザーをフォローする例です。

/users/相手のユーザー名/follow

ただ、これはより細かく階層を設けて、以下のように表現することもできるでしょう。

/users/相手のユーザー名/followers/自分のユーザー名

パラメータの受け渡し

ここからは、個別の論点について取り上げます。

まずはパラメータの受け渡し方法についてです。主にリソースに条件付けをして、「たくさんあるうちのどれか」を指定するパラメータ(変数)をサーバに渡す方法は、HTTP メソッドを問わずに言うと、一般的に「クエリパラメータ」「パスパラメータ」「リクエストボディ」が考えられます。

クエリパラメータ

これは GET の場合のみですが、URL に ? に続くパラメータが与えられるパターンです。

/products?category=12&color=34&size=m

検索機能での用例を目にしたことがあると思います。

パスパラメータ

パスの一部にパラメータを含めるパターンです。

/products/123

大抵、リソースを一意に特定する ID などの値が指定されます。

リクエストボディ

もうひとつ、これはエンドポイントではありませんが、リクエストボディにリソースを特定できる ID を含める方法も、論理的には考えられます。

PATCH /users

{"id": "123", "email": "foo@bar.com"}

クエリパラメータの反対に、これは POST PATCH PUT DELETE の場合ですね。

クエリとパスの使い分け

クエリパラメータとパスパラメータの使い分けについては、以下の基準で OK でしょう。

  • 任意のパラメータにはクエリパラメータ
  • 必須のパラメータにはパスパラメータ

ざっくり言い換えると、

  • クエリパラメータは検索系
  • パスパラメータは個別のリソースを特定

という解釈でも OK かもしれません。

そのパラメータがなくてもリソースとして成り立たつ、意味が変わらないのがクエリパラメータ、それがないとリソースとしての意味が変わってしまうのがパスパラメータ、という表現もできます。

ただし、どちらつかないような場合も存在します。たとえば以下の例を見てください。あるユーザーのツイート一覧を取得するエンドポイントです。

GET /tweets?user=john123
GET /users/john123/tweets

tweets を起点にすれば、ユーザーを検索条件と見なしてクエリパラメータとして表現できそうですし、users を起点にすれば階層構造を利用してパスパラメータとしても表現できます。

どちらがいいかは要件によるでしょう。その API の使いどころがそれこそツイートの検索機能であればクエリパラメータが良さそうですし、他の条件がありえない、個別のユーザー別ツイートページであれば、パスに含めるのが良いでしょう。

この例のように、形式だけではどちらが好ましいか判断できないパターンもあります。そこは要件と相談しながら、設計判断しましょう。

リクエストボディとパスの使い分け

大抵の場合で、パスを選択するのが好ましいです。

なぜなら、リクエストボディに ID などを含めてしまうと、URL をリソースとして見ることができなくなる、意味をなさなくなるからです。

POST? PATCH? PUT?

ここでは、POST PATCH PUT の使い分けについて見ていきます。

POST(追加)と PATCH(編集)は使い分けにあまり迷うところはないと思うので、それぞれと PUT(置換)の使い分けを取り上げます。

PUT とは

「置換」とは、「なかったら作る、あれば上書きする」という意味です。

たとえば、AWS の S3 を例として考えてみてください。AWS が公開する API では、S3 バケットに対するファイルの格納は、PUT で定義されています。foo.png という名前のファイルを PUT すると、バケット内にその名前のファイルが出来上がります。

では、もう一度同名のファイルを PUT するとどうなるでしょうか?もうひとつファイルができるのではなく、既存の foo.png が上書きされます。

このように、PUT の場合は何度呼び出しても対象は2つ以上に増えません。

POST と PUT

POSTPUT については、複数回実行したとき、リソースが増えるのか、増えないのかで使い分けるとよいでしょう。

たとえば、先に例に挙げた「いいね」の例で考えます。普通ありそうな仕様だと、ある対象に対しての「いいね」は1人1回まででしょう。その場合は PUT が良さそうです。

PUT /tweets/9876543/like

しかし、特殊な仕様で、1人がある対象に対して何度も「いいね」できてカウントアップしていく場合は、POST が適切になります。

POST /tweets/9876543/like

PATCH と PUT

次に PATCHPUT の使い分けです。

一部更新である PATCH のエンドポイントは、PUT でよりシンプルになる可能性があります。

たとえば、ユーザーの設定画面(マイページみたいなイメージ)で、配信メールを受け取るかどうかの設定を変更するエンドポイントを考えてみましょう。

配信メールを受け取る
PATCH /settings

{
  "receiveEmail": true
}

設定(settings)リソースの一部を更新するので、上記でも間違ってはいないのですが、PATCH は実装上、少しだけ厄介と言えます。

と言うのも、たとえば receiveEmail 以外の項目が渡ってきたときはどうするのでしょう?存在しない項目や、ユーザー側でいじってほしくない項目だった場合は?

包括的な編集機能は必要なく、「配信メールの設定変更」という機能だけあれば OK なのであれば、PUT を使って以下のようにも表現できるでしょう。

配信メールを受け取る
PUT /settings/receive-email

ちなみに配信メールを OFF にする場合は DELETE を使って表現できます。

配信メールを受け取らない
DELETE /settings/receive-email

包括的な編集機能が必要な場合であっても、PUT は一考の価値があります。

というのも、その場合はおそらく入力欄が並んだ編集画面があるのでしょう。PATCH の場合は「一部更新」なので、更新すべきデータのみ渡すほうが意味的に好ましそうです。

ここで実装を想像してみてください。複数の入力欄から、更新があった項目のみ選別して API に渡すのは少し面倒ですよね。だったら細かいこと言わずにすべて渡せばいいじゃないか、という判断もアリですが、それなら意味的に PUT = 置換のほうが近いのではないでしょうか。

いずれにせよ、追加・更新系の機能に出くわしたら、これは POST でやるとどうなるか?PATCH はありえるか?PUT だとどうか?と考え、色々なパターンを探るのがよいです。設計とは選択肢から選び取る活動なので、選択肢を出してみるのがまずは重要です。

DELETE

最後に DELETE についてです。

今まで何度か例に挙げていますが、DELETE はリソースそのものを削除する場合だけでなく、何かの設定などを OFF する場合にも使えます。

悪くはないが...
PUT /tweets/9876543/unlike
こちらのほうがいい
DELETE /tweets/9876543/like

ON が PUT で OFF が DELETE といった対で表現するパターンを覚えておくと便利です。

アンチパターン

たまに見かけるアンチパターンを紹介します。

無意味なリソース

NG例
GET /products/search?www=xxx&yyy=zzz
GET /products/123/detail

上の例において、searchdetail は不要です。

パスの一部が無くても意味が通じるかどうか、見直して考えてみましょう。

階層構造が不正

NG例
GET /users/profile/1234

なぜか変数を後ろに寄せたがる人がいますが、そういう問題ではないです。

階層構造に忠実に。

GET /users/1234/profile

その他の論点

その他、ちょっとした形式上の論点を紹介します。

大文字か? 小文字か?

まずは URL に大文字を許可するか?という論点です。

/foo-bar
/foo_bar
/fooBar

個人的には、URL はすべて小文字のスネークケースまたはケバブケースで統一します。

大文字を含めると、では大文字と小文字を区別すべきか?という、ユーザーの体験や価値とは関係のない議論に繋がると思うからです。

単数形? 複数形?

パスを単数形で表現するか、複数形で表現するか、という論点です。

/user/john123
/users/john123

あまり目くじら立てたくないですが、どちらかというと、階層構造に着目すべきと思います。

つまり、階層の考え方からすると、上位が下位を包含しているべきなので、ほとんどの場合で複数形が好ましいです。


以上、Web API のエンドポイントを設計する際の注意点などをまとめました。

これから SPA を開発する方の参考になれば嬉しいです。