こんにちは。7月にHolmesにジョインした友野です。サーバーサイドエンジニアをしています。
今回はREST API実装時のHTTPレスポンスステータスについてです。
基本的なことですが、重要なことなので備忘録もかねて記事にしておきます。
余談ですが、Holmesでは秋口のUI/UXリニューアルに向けて開発を進めています。ゴールに向けて、複数のスクラムチームが並行して開発をしているため、各々のチームで経験に基づいた暗黙知による実装差分が稀に発生します。
都度共有もしていますが、エンジニアが増えてきた今だからこそ、「ちゃんと決めなきゃね」という話から今回の内容も改めてまとめておこうと思い立ちました。
利用するHTTPメソッド
GET
、POST
、PUT
、DELETE
の4つです。リソースの更新はPUT
で行い、PATCH
は使いません。
べき等性を意識するという側面もありますが、なによりシンプルに実装を進めるためです。
レスポンスステータス
チームで話をするきっかけになったのが、このレスポンスステータスです。
正常時、GET
は200 OK
、POST
は201 Created
を返すという認識は合っていたのですが、
PUT
のAPIを実装する際に、
200 OK
を返す204 No Content
を返す
の2つに意見が分かれました。 宗教戦争、とは言いませんが、様々なバックグラウンドを持つメンバーから成るチームだからこそあり得ることです。
定義がどうなっていたか、改めてRFCを確認してみます。
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 OK
と204 No Content
のいずれかで示さねばならないとあるのみでした。
これを踏まえて、開発チームではPUT
で既存のリソースを更新する場合は、204 No Content
を返すと取り決めました。
200 OK
ではなく、204 No Content
を選択した理由は、特に返却する情報はないと考えたためです。
リソースが必要な場合は改めて取得し直すべき、と判断しました。
なお、リソースを作成する場合は明示的にPOST
を利用するので、PUT
によるリソース新規作成はしません。
レスポンスヘッダー/レスポンスボディ
さて、そうするとレスポンスヘッダーとレスポンスボディについても明らかにしておいた方が良さそうです。 こちらもRFCを念のため確認した上で、特に意見も分かれなかったので以下としました。
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では現在エンジニアを募集しています。 興味がある方は是非こちらからご連絡ください!