happy developing

楽しい開発ライフ

Web APIの設計を読んだ感想

読んだ本

f:id:yamaguchi7073xtt:20201227182436p:plain
https://www.shoeisha.co.jp/book/detail/9784798167015

本書を読みながら、参考になったところ、普段自分が開発していて思ったところなんかを書いていきます。
本書でいうところのWeb APIはもっぱらRESTが前提です。(GraphQL,gRPCについても言及はされています)

きっかけ

普段は自社プロダクト用のREST Endpointを定義することが多いのですが割とのりでやってしまっておりこの本で考えておくべき要素なんかが学べればいいなと思い読んでみることにしました。

まとめ

APIを決めるときに考えておくべきことの概要をつかめるのではないでしょうか。 ただし、なされる問題提起に対して基本的に答えは、(状況次第 | あなたが決める | ...)なのでいいからベストプラクティスだけ教えて的な読み方はできませんでした。

  • どのようにして正しいキャッシュ期間を決めればよいのだろうか。そう、状況次第である。
  • この問題(ドキュメントを実装から生成するか独立して管理するか)に関しては、どの方法がよいか悪いかではなく、設計者と組織にとってうまくいく方法を選択する必要がある。等々

第1部 APIデザインの基礎

第1章 APIデザインとは何か

そもそも、APIとはなにかみたいな話から始まります。
Web(IT)の勉強をはじめたときAPIっていわれてピンとこなかったのでそもそも"APIとは"にしっかり答えてくれている本は意外と貴重なのではと思いました。
ちなみに、自分の中での"API"とはの答えは、もちろん文脈依存ではあるのですが、json返すhttp serverです。
本書では以下のように書かれています。

APIは、どのような種類のものであれ、何よりもまずインターフェイスであり、2つのシステム、対象者、組織などが出会いやり取りするポイントである。

やはり概念自体が曖昧なのでわかったようなわからないような説明になってしまいますよね。
11章では

Web APIを簡単に要約すると、単体の同期型のリクエスト/レスポンス + REST + HTTP/1.1 + JSON Web APIになるだろう。

とあります。 第1章は普段からAPIを開発したり使ったりしている開発者の方よりもAPIについてあまりピンときていない方(例えばビジネスの方)に対する説明としてとてもよいのではと思いました。

第2章 ユーザを意識したAPIを設計する

APIを実際に利用するコンシューマの視点とAPIを提供する側の組織、ソフトウェアのプロバイダの視点という観点が導入されます。
要は実装の都合を隠蔽しようということだと理解しているのですが、プロバイダの視点のみにもとづいてつくられたAPIがいかに使いにくいかが電子レンジの具体例でこれでもかと説明されます。

APIのゴールリストとデータがデータベースとあまりにも一致しているとしたら、そのAPIをプロバイダの視点にたって設計している可能性がある。

これは意外とやってしまいがちであったり、そういう実装をみたりするので気をつけたいです。
まとめると、APIをみるとプロバイダのデータ、コード、ビジネスロジック、ソフトウェアアーキテクチャ、人間組織がわかってしまうのを避けようということだと思います。

コンウェイの法則として、システムを設計する組織は、組織のコミュニケーション構造にそっくりの設計を生み出すという格言が紹介されていました。
自分は大きな組織で働いたことがないのであまりぴんとこなかったのですがこれはどの程度普遍的なんでしょうか。

第3章 プログラミングインターフェイスを設計する

RESTについて丁寧に説明してくれています。ここでは割愛しますが、はじめてAPI設計される方にも本書はおすすめできるなと思いました。

CRUDのHTTPメソッド(POST, GET, PUT, PATCH, DELETE)を単なるCRUDを超えたアクション、あるいはあまりCRUDではないアクションを表すために使わなければならないこともある。
と述べられており、これは場合によってはベテラン設計者にとっても難しい問題とされています。
gRPCでAPIを定義できると処理をメソッドとして素直に表現できるのですがそれをRESTに翻訳しようとするとHTTPメソッドで悩んだり、あえて中間的なリソース(処理のリクエストとか)を表現してそれを作成している体にしたりするなと思いました。

本書でも

設計者がREST リソースでのアクションをHTTPメソッドにマッピングできない場合、最初の選択肢はたいていアクションリソースを作成することである。

とあり、ショッピングカートのチェックアウトをPOST /cart/checkout, /check-out-cartで表現することを提案しています。
また、POSTはユースケースに適したメソッドがない場合のデフォルトのメソッドであるとしています。

より、RESTモデルに近づけるにはPATCH /cart status = CHECKING_OUTという方法もあるが、チェックアウトする方法がわかりづらくなってしまう点を指摘しています。
大切なことは、ユーザーフレンドリ性とAPIスタイルへの準拠にはトレードオフがあることを認識しておくことのようです。

RESTに関しては以下のドキュメントを読むことがオススメされています。

第4章 API記述フォーマットを使ってAPIを記述する

この章ではOpenAPI Specification(OAS)の書き方について丁寧に解説してくれています。
自分も最近、Swagger(2.0)からOAS(3.0)を使うようになりました。
はじめてOASを書かれる方はこの章で概要を掴んでから、公式のドキュメントをみながら書いてみるのがよいのではないでしょうか。

description propertyはめんどくさかったり、見ればわかるだろうと思ったりしますが、書けるだけかいておいたほうがいいというのが自分の数少ない知見です。
OASを手で書くか生成するかについては後にふれられます。

第2部 ユーザブルなAPIの設計

第5章 単純明快なAPIを設計する

名前の付け方

大切なのはコンシューマーが理解できること。

  • 略語は一般的なもの以外たいていよい考えではない。
  • booleanといった型はドキュメントみればわかるので名前にいれない。
  • コンテキストを利用してよい。(user.userName -> user.name)

使いやすいデータ型とフォーマット

  • 事前に計算された付加価値をもつデータを提供すればコンシューマ側で何かをおこなったり推測したりしなくてよくなる。
  • /accounts/<UUID> より /accounts/<AccountNumber> のほうがユーザフレンドリーとされる。

Timeデータどうするか問題

UNIXタイムスタンプよりISO 8601の文字列のほうがずっとユーザフレンドリーであるとされています。 "2015-02-07"のようにタイムゾーン取り違えのリスクを減らすために必要がないときは時刻値を提供しないことを勧めるとあるが むしろ、UTCなのかJSTなのかわからないので、RFC3369のように完全な情報でよいのではと思いました。

Enumどうするか問題

数値コードを使うのはたいていよくない考えである。(開発者が絶えずドキュメントを調べるはめになるから)
0じゃなくて、"type": "checking"
なんらかの理由で数値を使う必要があるときは、typeName propertyも追加してあげるとよい。

エラーフィードバック

エラーは以下のように分類できる。

  • malformed error 必須パラメータがなかったり、データの型が違ったり。

  • functional error いわゆるビジネスロジックによるエラー。

  • server error 実装のバグだったりDBが落ちていたり。

コンシューマの視点からはserver errorは一つ特定しておけばたいてい十分。
RFC7231によればコンシューマに起因するエラーは4XX, プロバイダに起因するエラーは5XXを使う。

  • malformed errorは400(Bad Request)
  • functional error
    • 403(Forbidden)
    • 409(Conflict)
  • server errorは500(Internal Server Error)

HTTPステータスコードだけでは不十分問題

表現しようとしているエラーに対応するステータスコードがあればよいがぴったりこないものもある。
Formのfield AがValidation違反であることを伝えたい場合、400だけでは足りない。
結局HTTPステータスコード以外にエラーを識別する情報が必要。

本書では以下のようなエラーレスポンスが例示されている。

{
  "source": "firstname",
  "path": "$.owners[0].firstname",
  "type": "MISSING_MANDATORY_PROPERTY",
  "message": "Firstname is mandatory"
}

ユーザに表示するエラーメッセージをbackendから返すかフロントで定義するかは判断がわかれるところだと思うがどちらにせよ エラーを識別する情報(ここではtype)が必要。
そうなってくるとエラーにぴったりくる4XXのエラーコードを返す必要性が薄れてくるんじゃないかと思う。
ただし、HTTPステータスコードはfrontとbackend間の様々なコンポーネントにも伝わる情報なので、全部500にするとそれはそれで問題が起きる。

ということで自分の結論:

  • リソースが見つからないNotFoundや認証/認可エラーはそれぞれステータスコード対応させる。(それ以外も可)
  • 他は全部400
  • エラーにはエラーを識別する情報を付与する

こうしておけば、1日のNotFound数が急に増えてきているみたいな情報がメトリクスから拾えつつも、HTTPステータスに悩まなくても済む。
ただこの方法をとると、productionのformによるリソース作成の場面なんかでは結局すべてのfieldになんらかのvalidationがあるのでfieldごとにエラー識別情報を定義する必要がでてきてしまう。最大文字数なんかはフロントでかけれると思うかもしれないがCSV等のバッチ作成だったりAPI連携だったりでフロント介さないルートも生まれてくるので結局backendで識別する必要がでてくる。

エラーを複数返さないといけない問題

エラーがひとつだけの場合は上記の方針でよさそうと思ったものの、実際にはValidation違反なんかは複数おきる。
さらにエラーをHTTPステータスコードで分類していた場合、どちらかのレスポンスで返す必要がある。(もしくはそれ以外)

自分の結論:

  • HTTPステータスコードごとにレスポンスの型をきる
  • Validation系のエラー(functional error/400 Bad Request)は複数のエラー返せるようにしておく

実装的にも認証/認可と処理に必要な情報のfetchしてからvalidationロジック走らせるのでこういう形に落ち着いた。

成功のフィードバック

単なる確認応答ではなくコンシューマに有益な情報を提供するものでなければならない。
作成されたリソースの情報が何もかもふくまれているべきであり、次のステップで役立つかもしれない情報を提供するとよいとあります。

個人的にも、作成されたリソースの情報をすべて返してくれるとテスト書きやすくていいなと思います。

第6章 予測可能なAPIを設計する

一度も訪れたことのない建物でドアを開ける方法を知っているのはなぜだろうか。同じようなドアを前にあけたことがあるからだ。

一貫性

ばらつきや矛盾のない一貫性のある設計が重要であると言われています。
一貫性のない具体例として、同じ情報がエンドポイントごとに違う名前になっている例が挙げられています。
この要請はわりとfield名にコンテキストだしてよいとする考え方とぶつかるのでこのあたりが悩ましいところですね。

/accounts/{accountNumber}/transfers/delayed/{transferId}のようにURLの階層レベルが違うことも一貫性に反する例として挙げられています。
(この場合/delayed-transfers/{transferId}にするとか)

一貫性の4つのレベル

今までの話は基本的にAPI「内部」の一貫性で、APIがもつべき一貫性はこれにとどまらず以下のレベルがあるとされています。

  • レベル1: API内部での一貫性
  • レベル2: 組織、企業、チームのAPIにまたがる一貫性
  • レベル3: APIの問題領域での一貫性
  • レベル4: 外の世界との一貫性

レベル2に関しては設計者の実力以外にもAPIプロバイダ組織のチーム力のようなものが問われてきそうです。 13章でもAPIサーフェスの一貫性を保つにはAPIの設計者どうしが強力する必要が挙げられています。 レベル3についてはドメイン知識をつけるしかなさそうだなと思いました。
レベル4では標準規格があればできるだけ準拠することも含まれるみたいです。(再生ボタンの三角形はISO 7000で定義されている)

たとえ組織内のAPI設計者があなた一人でもAPIデザインガイドのようなドキュメントを整備する必要性が説かれています。
自分もデザインガイドで命名規則を決めるところからはじめてみようと思っています。

適応性

Accept/Content-Typeを利用したコンテンツネゴシエーション、Accept-Language/Content-Languageによる国際化と地域化についてふれられています。

発見性

paginationに関する情報(今何ページ目、合計のリソースカウント)や、次に有効なアクションを返すといった「今どこにいて、何ができるか」に関する情報(メタデータ)を返すことが提案されています。
今までは、pagination系の情報しかメタ情報として返すAPIしか作っていなかったのでこの考えはとても参考になりました。

APIのレスポンスに"href": "/resource/123/actions"のように関連する情報をlinkの形で提供することでAPIの発見可能性を高める考えが紹介されています。
恥ずかしながらこのあたりに知見がなく、是非ベストプラクティスが知りたいと思っていました。
ただ本書では、HAL, Collection+JSON, JSON API, JSON-LD, Hydra, Sirenといったフォーマットが知られているが標準のようなものはないと書かれており、特定の方式を推しているということはありませんでした。
(HATEOASの発音は著者であってもわからないらしいです)

第7章 うまく整理された簡潔なAPIを設計する

レスポンスのjson fieldについて以下のような視点からの整理が提案されており参考になります。

  • 関連する情報はobjectに切り出す
  • 関連する情報は近くにする
  • 重要度が高い順にならべる

複数のエラーを返す場合には、重要なエラー順にソートすることも提案されています。
個人的にはこれはちょっとやりすぎな気もしました。(なかなか優先度がつけられないエラーも多いんじゃないかなと)

第3部 コンテキストに応じたAPIデザイン

ここでいうコンテキストとは以下のような視点と理解しています。

  • ネットワークを有効利用できているか
  • セキュアか
  • 破壊的な変更をさけられるように設計されているか
  • ドキュメント化できているか

第8章 セキュアなAPIを設計する

APIのセキュリティについては一冊の本になってもおかしくないので本書はオーバビューとして位置づけられています。
OAuth 2 in ActionAPI Security in Actionが紹介されていました。

APIのゴール(endpoint)をどういう単位で切るかを考えるときにスコープを考慮にいれておくことが大事そうです。
スコープのポリシーも参考になりました。OASだとscopesとして定義できるのもよいですね。

スコープに加えてもうひとつ検討しておくべきことがあります、それがAPIのリクエスト/レスポンスに含まれるセンシティブなデータの取り扱いです。
これを雑に扱っているサービスが時々話題になったりしますね。
なにがセンシティブなデータにあたるかはドメイン(業界や産業)によるので必ずCISO(Chief Information Security Officer)、DPO(Deta Protection Officer)、CDO(Chief Data Officer)または法務部に相談しようとアドバイスされています。
皆様の職場にはこういった役職の方々おられますでしょうか。

リソースへのアクセスに対して403 Forbiddenを返すことはときに暗黙的にそのリソースの存在を認めていることになるから情報漏洩とみなされることがあると注意されています。
クレジットカード番号とかは明らかですが、ドメインを理解していないとやってしまうかもしれないので注意したいです。

GETのクエリパラメータにもセンシティブな情報いれないように注意されています。
GET /accounts?customerLastName=yutaのようにやってしまうと、コンシューマとプロバイダ間のすべてのHTTPログで追跡されてしまいます。
これを防ぐにはPOST /accounts/searchでリクエストボディに検索パラメータを配置するのがもっとも安全だろうとしています。
こうなってくると検索系のエンドポイントはじめからPOSTにしたくなってくるような気もしますがREST的にどうなんでしょうか。

第9章 APIの設計を進化させる

破壊的変更(コンシューマがコードを変更しないと問題が起きる変更)をどうやって避けるかについて述べられています。
細かく変更が分類されていますが(プロパティ名の変更、必須からオプショナルに、型の変更、列挙に値を追加...)、結論だけいうと、新しい要素を追加する以外全て破壊的変更になるという話だと思います。

リダイレクトさせることにも否定的です。理由は、クライアント側がリダイレクト(301 Moved permanently)に従う設定をしている保証がなかったり、リクエストが勝手に転送されるくらいならエラーにしたりする場合があるからだそうです。

引用されているハイラムの法則では以下のようにのべられています。

APIに十分な数のユーザがいれば、コンストラクタで何を約束するかは問題ではない。システムの目に見える振る舞いはすべて誰かに依存することになる。

APIのバージョニング

セマンティックバージョニングはAPIの実装には適していても、コンシューマの視点からは破壊的なレベルの数字(メジャーバージョン)だけが重要であることが説明されています。
とすると、バージョン名は数字である必要がなく自由に決めることができて、2017-10-19のような日付も使うことができるとされています。
AWSのCloudFormationが日付でバージョン指定していたのはこういう理由だったからなのかもしれないです。

第10章 ネットワーク効率のよいAPIを設計する

ネットワーク効率に関わるトピックを扱います。
Cache-ControlETagでのcacheコントールについての具体例なんかが載っています。
これって実際にやろうと思うとAPIがとても複雑になりそうだと思うのですがみなさんどうされてるんですかね、このあたりの知見はとても気になるのでご存知の方がおられましたら教えていただきたいです。

フィルタリング

/accounts/A1/transactions?page=2&size=25のようなページベースリクエストについて
各取引を調べてすでに取得済かどうかをチェックし、重複しているデータを無視する責任はコンシューマにあると(珍しく)名言してくれています。
ReactだとListのItem系のコンポーネントのid propertyにリソースのID渡せばとくに意識しなくてよいんですかね。
リクエスト間での重複データが許容できない場合は、カーソルベースのページングが提案されています。
AWSとかだとこの方式ですよね、ただこの方式ですとUI上のページネーション?で3ページ目をいきなり取得したりすることができなそうなので悩ましいです。

リストと詳細

テーブル系のコンポーネントにリソースの一覧を表示して、選択したら詳細画面に遷移する基本的なUIを実現したいときにリストのレスポンスにどこまで情報のせるかという問題について。
本書では、通常は各要素の概要が消されるがそうしなければならないと決まっているわけではない。完全な表現を返すほうが効率的なこともあると述べて、具体例を紹介してくれています。
集約データのTTLはレスポンスのプロパティのうち最小のTTLになるので、レスポンスで返す情報が増えるほどキャッシュの可能性を妨げることになると注意されています。

リストにすべてのリソースの情報いれる以外どうしても対応できないユースケースがでてきてしまうのは避けられないので、クライアントが自身で欲しい情報を選択できるようにしたくなります。
一つの案として、Accept: application/vnd.bankingapi.extended+jsonのようにAcceptヘッダーでリソースの欲しい情報をクライアントから指摘できる仕組みがあげられています。(ここでは、extended, summarized, complete)
これは標準の手法ではなく完全なカスタムメディアタイプであるそうです。

ここまでしたくなったら、GraphQLがいいのではと思っていたらGraphQLの例も紹介されていました。
GraphQLについては自分の2021年の課題です、React/Front側のclient libraryは充実しているのでbackend側も対応できるようにしてFront -> GraphQL -> gRPCがいいんじゃないかなと自分としては考えています。
本書でも、APIレイヤという文脈で具体的なユースケースにあわせたエクスペリエンスAPIと特化していないオリジナルAPIのようなレイヤードな設計についてふれられています。

第11章 コンテキストに基づいてAPIを設計する

リクエストに対してすぐにレスポンスを返す意味での同期的なAPI以外についてふれられています。

Web Hook

コンシューマにポーリングさせるのではなくあらかじめ登録されたURLにプロバイダが通知したいイベントをPOSTリクエストするWeb Hookについて説明されています。
イベントごとにWeb Hook URLを用意するのか汎用的なURLを用意しておくかはニーズによるが、軽量で汎用的なイベントをうけとるWeb Hookをひとつ用意しておくのがよい戦略であるとされています。
理由は、新しいイベントの追加が容易であることと、イベントを軽量に保っておきコンシューマが詳細を取得するためにプロバイダを呼び出す方式のほうがセキュアだからのようです。
また、リクエストの暗号化と署名やmTLSといった様々な保護が紹介されています。(SlackのWeb HookもTLS(https)が必須でした。)

Web Hookには規格はないそうですが、WebSubというW3Cが発表している勧告があるそうです。

イベントストリーム

サーバからクライアントに情報を通知したい場合、自分はWebSocketしか知らなかったのですがSSE(Server-Sent Event)という方法があることを知りました。
HTTPプロトコルでレスポンスデータに情報を流し続けることでHTTPベースでイベント情報を通知できる仕組みのようです。(そのためサーバ -> クライアントの一方向のみ)

複数の要素の処理

1APIリクエストで複数のリソースを処理(作成/更新)する場合、特にそのリソースの処理の一部が失敗した場合どのようなレスポンスを返すべきかについて。
本書では、正常に処理できるものはできるだけ処理してすべてエラーを返すことを提案しています。
また、そのためのステータスコードとして207(Multi-Status)を利用したjsonのレスポンス例を載せてくれています。

実際に207使うかは悩ましいですが、処理の一部が失敗しても成功系のステータスコード返して呼び出し側に成功/失敗したそれぞれのエントリーを返すのがよいというのはそのとおりだと思います。
AWSのBatch系のAPIも概ねそんな感じで実装されています。

第12章 APIを文書化する

APIのドキュメント化について。OASで作成しておけばドキュメント作成でもエコシステムの恩恵にあずかれます。
自分は本書でも紹介されているReDocを利用しています。

npx redoc-cli bundle ./openapi.yaml --output ./static.html

TwiilioStripeのドキュメントがおすすめされていました。

ドキュメントを実装から生成すべきか否か

APIドキュメントを実装(コメントやAnnotationも含む)のみにもとづいて生成できれば実装とドキュメントの同期を保つことができます。
しかしながら本書ではいくつかその方法の欠点も指摘されていました。

  • 既存のアノテーションフレームワーク(少なくとも著者が利用されてきもの)ではAPI記述フォーマットを直接操作するときのような柔軟性は得られない。
    (汎用的なデータ構造の例をコンテキストにあわせたり)
  • ドキュメントを修正するために実質的にコードの変更が必要になる。
  • ドキュメント生成のために早い段階からコードを実際に書かなければならない。

結論としてはどの方法がよいか悪いかではなく、設計者と組織にとってうまくいく方法を選択する必要があるとのことでした。

第13章 APIを成長させる

6章のAPIでの一貫性でもふれられていた組織のAPIガイドラインの重要性が説明されています。
ここでいうガイドラインとは、設計者全員が従うルールを集めたものと定義されています。
作って終わりではなく変更(そして廃止にも)前向きであろうと注意が促されています。
(ガバナンスの暗黒面とか、API警察という表現がおもしろかったです)

Web関連のRFCがまとまっているWeb Conceptsというサイトが紹介されており非常に参考になりました。
例えば[HTTP Headerごとに関連するRFC](http://webconcepts.info/concepts/http-header/がのっていたりします。

レビュー時のチェックリストの項目も参考になりました。チーム(組織)で作っておくとよさそうだなと思いました。

おわりに

もうすこし実装よりの話が読みたい人にはReal World HTTPがオススメです。
自分はまだ第2版読めていませんが。