ContractS開発者ブログ

契約マネジメントシステム「ContractS CLM」の開発者ブログです。株式会社HolmesはContractS株式会社に社名変更しました。

REST APIのレスポンスステータスについて改めて考える

こんにちは。7月にHolmesにジョインした友野です。サーバーサイドエンジニアをしています。

今回はREST API実装時のHTTPレスポンスステータスについてです。
基本的なことですが、重要なことなので備忘録もかねて記事にしておきます。

余談ですが、Holmesでは秋口のUI/UXリニューアルに向けて開発を進めています。ゴールに向けて、複数のスクラムチームが並行して開発をしているため、各々のチームで経験に基づいた暗黙知による実装差分が稀に発生します。
都度共有もしていますが、エンジニアが増えてきた今だからこそ、「ちゃんと決めなきゃね」という話から今回の内容も改めてまとめておこうと思い立ちました。

利用するHTTPメソッド

GETPOSTPUTDELETE の4つです。リソースの更新はPUTで行い、PATCH は使いません。 べき等性を意識するという側面もありますが、なによりシンプルに実装を進めるためです。

レスポンスステータス

チームで話をするきっかけになったのが、このレスポンスステータスです。
正常時、GET200 OKPOST201 Createdを返すという認識は合っていたのですが、 PUTAPIを実装する際に、

  • 200 OKを返す
  • 204 No Contentを返す

の2つに意見が分かれました。 宗教戦争、とは言いませんが、様々なバックグラウンドを持つメンバーから成るチームだからこそあり得ることです。

定義がどうなっていたか、改めてRFCを確認してみます。

RFC7231 PUT *1

If the target resource does have a current representation and that representation is successfully modified in accordance with the state of the enclosed representation, then the origin server MUST send either a 200 (OK) or a 204 (No Content) response to indicate successful completion of the request.

リクエストが正常に完了したことを200 OK204 No Contentのいずれかで示さねばならないとあるのみでした。 これを踏まえて、開発チームではPUTで既存のリソースを更新する場合は、204 No Contentを返すと取り決めました。

200 OKではなく、204 No Contentを選択した理由は、特に返却する情報はないと考えたためです。 リソースが必要な場合は改めて取得し直すべき、と判断しました。
なお、リソースを作成する場合は明示的にPOSTを利用するので、PUTによるリソース新規作成はしません。

レスポンスヘッダー/レスポンスボディ

さて、そうするとレスポンスヘッダーとレスポンスボディについても明らかにしておいた方が良さそうです。 こちらもRFCを念のため確認した上で、特に意見も分かれなかったので以下としました。

RFC7231 POST
RFC7231 DELETE

  • 201 Createdを返却する場合
    • Locationヘッダーに作成したリソースのURIを合わせて返却する
    • レスポンスボディは不要
  • 204 No Contentを返却する場合
    • レスポンスボディは不要

まとめると、以下の通りです。

HTTPメソッド レスポンスステータス 備考
GET 200 OK 単一/複数リソース取得に利用する
POST 201 Created Location ヘッダーに作成したリソースのURIをセットする
レスポンスボディは不要
200 OK 複雑/組合せの条件などクエリパラメータではリクエスURIの文字数に懸念がある場合*2にリソース取得(検索)時にも POST を利用する
PUT 204 No Content 既存のリソースを更新する
レスポンスボディは不要
DELETE 204 No Content 既存のリソースを削除する
レスポンスボディは不要

Springによる実装イメージ

レスポンスボディを持たない実装イメージは以下の通りです。

  • 201 Created
@PostMapping("/api/tasks")
public ResponseEntity<Void> createTask(@RequestBody CreateTaskCommand command) {
    String taskId = createTaskService.handle(command);
    URI location = UriComponentsBuilder.fromUriString("http://example.com/api/tasks/" + taskId).build().toUri();
    return ResponseEntity.created(location).build();
}
  • 204 No Content
@DeleteMapping("/api/tasks/{taskId}")
public ResponseEntity<Void> deleteTask(@PathVariable("taskId") String taskId) {
    deleteTaskService.deleteWith(taskId);
    return ResponseEntity.noContent().build();
}

最後に

当たり前だと思っていたことが、わずかな思い違いが事故につながることも少なくありません。このような事故を未然に防ぐために、少しずつ明文化していきたいと思います。

Holmesでは現在エンジニアを募集しています。 興味がある方は是非こちらからご連絡ください!

lab.holmescloud.com

*1:当該RFCは執筆日現在で"PROPOSED STANDARD"ステータスです

*2:RFCでは最大文字数は定義されていないようですが、サーバーやブラウザの実装に依存するのを回避するためです