isyumi_netブログ

isyumi_netがプログラミングのこととかを書くブログ

書き込み系のREST APIは、URLとBodyに無いデータを書き込まないようにしよう

REST APIは、要求を受理するか判定する部分と、要求に従ってデータを返信したりDBに書き込んだりする部分に分かれる。前者の受理判定部分はあらゆる情報を使っていい。しかし、後者の処理部分はリクエストに明記されたもの以外を使ってはいけない。使っていいのはURLとBodyだけだ。

例えば/myprofileというAPIがあったとしよう。このサーバーはCookieを使ってアクセスした人を特定できる。そこで /myprofile ではCookieの情報をもとにその人のプロフィール情報を返してあげていた。これはよくない設計だ。このように直してほしい。まずURLは/profiles/:user_id (:user_idは変数)に変更しよう。そして、メソッドの冒頭にCookieと:user_idを比較して一致しなければエラーを出してほしい。

void onMyProfile(HttpRequest req) {
// これで /users/:user_id の:user_idの部分が取れる
var userID = req.uri.pathSegments[1];
if (!isUserIDMatch(req.cookies, userID)) {
req.response.statusCode = HttpStatus.BAD_REQUEST;
return;
}

req.response
..statusCode = HttpStatus.OK
..write(getProfile(userID));
}

 前者と後者の違いは、前者はサーバーに忖度を求めているのに対し後者はサーバーにしてほしいことを明確に指定している。後者の考え方でシステム全体を統一することで設計の不明瞭さがなくなる。どのAPIも挙動が予想しやすく、変更をかけやすく、機能追加したい人がどうしたらいいかわかりやすい。もう一つの違いは、前者は処理が始まってから複数の成功パターンに分岐していくのに対して、後者は一種類の成功パターン以外はすべてエラーになるということだ。後者のほうがコードと仕様がきれいになる。

他に例を挙げると、掲示板の書き込みAPIはPostのBodyにMessageとUserIDを取ろう。Messageだけを受け取りUserIDをCookieから割り出してはいけない。

いい例
{
"message": "こんにちは",
"user_id": "user1"
}
悪い例
{
"message": "こんにちは"
}

 また、1年以上前から会員登録してくれていた人向けに特別サービスをするときのAPIについても考えよう。二種類考えられる。

案1

  • サーバー側で会員登録履歴を確認する
  • 1年以上前からの会員にサービス価格を適用する
  • それ以外の会員に通常価格を適用する

案2

  • 特別サービス対象者か確認できるGet APIを用意する
  • 購入APIではRequest Bodyで自分が対象者かどうか明記させる
  • 非対象者が対象者として購入APIをコールしたらエラーにする
  • 対象者が非対称者として購入APIをコールしたらエラーにする
  • それ以外は申告通りに適用する

この場合も、面倒だが案2を選ぼう。