REST APIを設計する際、すべてのエンドポイントで統一されたレスポンス形式を返すことは、クライアント側の実装を簡潔にし、API全体の一貫性を高めます。Spring Bootでは、ResponseBodyAdviceインターフェースを実装することで、コントローラのレスポンスをHTTPメッセージコンバータが処理する直前に加工できます。本記事では、共通レスポンスラッパーの設計から、supports()メソッドによる適用条件の制御、ページネーション情報の自動付与まで実践的な実装パターンを解説します。
実行環境と前提条件#
本記事のサンプルコードは以下の環境で動作確認しています。
| 項目 |
バージョン |
| Java |
21 |
| Spring Boot |
3.4.1 |
| Gradle |
8.x |
前提条件として、Spring Boot Webプロジェクトが作成済みであることを想定しています。spring-boot-starter-web依存関係が含まれていれば問題ありません。
1
2
3
|
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}
|
期待される学習成果#
本記事を読み終えると、以下のことができるようになります。
ResponseBodyAdviceインターフェースの役割と動作タイミングを理解できる
- 共通レスポンスラッパークラスを設計し、すべてのAPIレスポンスを統一形式でラップできる
supports()メソッドで特定のコントローラやパッケージのみに適用対象を絞り込める
- ページネーション情報やメタデータを自動的にレスポンスに付与できる
なぜレスポンス形式を統一するのか#
REST APIでは、各エンドポイントが異なる形式のレスポンスを返すと、クライアント側の実装が煩雑になります。
統一されていない場合の問題#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// GET /api/users/1
{
"id": 1,
"name": "田中太郎"
}
// GET /api/products
[
{"id": 1, "name": "商品A"},
{"id": 2, "name": "商品B"}
]
// POST /api/orders
{
"orderId": "ORD-001",
"status": "CREATED"
}
|
上記のように形式がバラバラだと、クライアント側では各エンドポイントごとに異なるパーサーを用意する必要があります。
統一された形式のメリット#
1
2
3
4
5
6
7
8
9
10
11
|
{
"success": true,
"data": {
"id": 1,
"name": "田中太郎"
},
"meta": {
"timestamp": "2026-01-04T17:00:00+09:00",
"path": "/api/users/1"
}
}
|
統一された形式にすることで、以下のメリットが得られます。
- クライアント側の実装簡素化: 共通のレスポンスパーサーを1つ用意すればよい
- エラー判定の統一:
successフラグやstatusコードで成功・失敗を判定できる
- メタ情報の一括管理: タイムスタンプ、リクエストパス、バージョン情報などを統一的に含められる
- 拡張性の確保: 将来的に新しいフィールドを追加しても既存クライアントへの影響を最小化できる
ResponseBodyAdviceとは#
ResponseBodyAdviceは、Spring MVCが提供するインターフェースで、@ResponseBodyやResponseEntityを返すコントローラメソッドの戻り値を、HttpMessageConverterが書き込む直前に加工できます。
ResponseBodyAdviceの位置付け#
以下の図は、リクエストからレスポンスまでの処理フローにおけるResponseBodyAdviceの実行タイミングを示しています。
sequenceDiagram
participant Client
participant DispatcherServlet
participant Controller
participant ResponseBodyAdvice
participant HttpMessageConverter
Client->>DispatcherServlet: HTTPリクエスト
DispatcherServlet->>Controller: ハンドラメソッド呼び出し
Controller->>DispatcherServlet: 戻り値(レスポンスボディ)
DispatcherServlet->>ResponseBodyAdvice: supports() 判定
ResponseBodyAdvice-->>DispatcherServlet: true/false
alt supports() == true
DispatcherServlet->>ResponseBodyAdvice: beforeBodyWrite()
ResponseBodyAdvice-->>DispatcherServlet: 加工後のボディ
end
DispatcherServlet->>HttpMessageConverter: 書き込み
HttpMessageConverter->>Client: HTTPレスポンスHandlerInterceptorとの違い#
HandlerInterceptorのpostHandle()メソッドでもレスポンス処理に介入できますが、@ResponseBodyを使用するREST APIではレスポンスボディの内容を変更することが困難です。ResponseBodyAdviceは、レスポンスボディがHttpMessageConverterに渡される直前に介入するため、JSONなどへのシリアライズ前にオブジェクトレベルで加工できます。
| 項目 |
HandlerInterceptor.postHandle() |
ResponseBodyAdvice.beforeBodyWrite() |
| 実行タイミング |
コントローラ実行後、ビューレンダリング前 |
HttpMessageConverter書き込み直前 |
| レスポンスボディ変更 |
困難(すでにコミット済みの場合あり) |
容易(オブジェクトレベルで変更可能) |
| REST API向き |
限定的 |
最適 |
| ヘッダー変更 |
可能 |
可能 |
ResponseBodyAdviceの実装方法#
ResponseBodyAdviceインターフェースには2つのメソッドが定義されています。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public interface ResponseBodyAdvice<T> {
boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType);
@Nullable
T beforeBodyWrite(@Nullable T body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response);
}
|
supports()メソッドの役割#
supports()メソッドは、beforeBodyWrite()を実行するかどうかを判定します。trueを返した場合のみbeforeBodyWrite()が呼び出されます。
主な判定条件として以下が利用できます。
- 戻り値の型:
returnType.getParameterType()で取得
- コントローラクラス:
returnType.getContainingClass()で取得
- メソッドのアノテーション:
returnType.hasMethodAnnotation()で判定
- コンバータの種類:
converterTypeでJacksonなどを判別
beforeBodyWrite()メソッドの役割#
beforeBodyWrite()メソッドは、レスポンスボディを加工して返します。引数として以下の情報を受け取ります。
| 引数 |
説明 |
body |
コントローラが返したオブジェクト(nullの場合あり) |
returnType |
コントローラメソッドの戻り値型情報 |
selectedContentType |
コンテントネゴシエーションで決定されたContent-Type |
selectedConverterType |
使用されるHttpMessageConverterのクラス |
request |
リクエスト情報(ServerHttpRequest) |
response |
レスポンス情報(ServerHttpResponse) |
共通レスポンスラッパークラスの設計#
まず、すべてのAPIレスポンスをラップする共通クラスを設計します。
ApiResponseクラスの実装#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
package com.example.demoapi.common.response;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.time.OffsetDateTime;
@JsonInclude(JsonInclude.Include.NON_NULL)
public record ApiResponse<T>(
boolean success,
T data,
ResponseMeta meta,
ErrorInfo error
) {
public static <T> ApiResponse<T> success(T data, String path) {
return new ApiResponse<>(
true,
data,
new ResponseMeta(OffsetDateTime.now(), path, null),
null
);
}
public static <T> ApiResponse<T> success(T data, String path, PageInfo pageInfo) {
return new ApiResponse<>(
true,
data,
new ResponseMeta(OffsetDateTime.now(), path, pageInfo),
null
);
}
public static <T> ApiResponse<T> error(ErrorInfo errorInfo, String path) {
return new ApiResponse<>(
false,
null,
new ResponseMeta(OffsetDateTime.now(), path, null),
errorInfo
);
}
}
|
メタ情報クラスの実装#
1
2
3
4
5
6
7
8
9
10
11
|
package com.example.demoapi.common.response;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.time.OffsetDateTime;
@JsonInclude(JsonInclude.Include.NON_NULL)
public record ResponseMeta(
OffsetDateTime timestamp,
String path,
PageInfo page
) {}
|
ページネーション情報クラスの実装#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package com.example.demoapi.common.response;
public record PageInfo(
int page,
int size,
long totalElements,
int totalPages,
boolean first,
boolean last
) {
public static PageInfo from(org.springframework.data.domain.Page<?> page) {
return new PageInfo(
page.getNumber(),
page.getSize(),
page.getTotalElements(),
page.getTotalPages(),
page.isFirst(),
page.isLast()
);
}
}
|
エラー情報クラスの実装#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package com.example.demoapi.common.response;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.List;
@JsonInclude(JsonInclude.Include.NON_NULL)
public record ErrorInfo(
String code,
String message,
List<FieldError> fieldErrors
) {
public record FieldError(
String field,
String message,
Object rejectedValue
) {}
}
|
ResponseBodyAdvice実装クラスの作成#
共通レスポンスラッパーを自動的に適用するResponseBodyAdvice実装クラスを作成します。
基本実装#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
package com.example.demoapi.common.advice;
import com.example.demoapi.common.response.ApiResponse;
import com.example.demoapi.common.response.PageInfo;
import org.springframework.core.MethodParameter;
import org.springframework.data.domain.Page;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@RestControllerAdvice
public class ApiResponseWrapperAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
// すでにApiResponseでラップされている場合は処理しない
return !ApiResponse.class.isAssignableFrom(returnType.getParameterType());
}
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
String path = request.getURI().getPath();
// Pageオブジェクトの場合はページネーション情報を付与
if (body instanceof Page<?> page) {
PageInfo pageInfo = PageInfo.from(page);
return ApiResponse.success(page.getContent(), path, pageInfo);
}
// 通常のオブジェクトはそのままラップ
return ApiResponse.success(body, path);
}
}
|
Stringレスポンスへの対応#
Stringを返すコントローラメソッドでは、StringHttpMessageConverterが使用されるため、ApiResponseオブジェクトを返すとエラーになります。この問題を解決するには、Stringの場合はJSON文字列として返すか、特別な処理を行います。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
package com.example.demoapi.common.advice;
import com.example.demoapi.common.response.ApiResponse;
import com.example.demoapi.common.response.PageInfo;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.MethodParameter;
import org.springframework.data.domain.Page;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@RestControllerAdvice
public class ApiResponseWrapperAdvice implements ResponseBodyAdvice<Object> {
private final ObjectMapper objectMapper;
public ApiResponseWrapperAdvice(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
return !ApiResponse.class.isAssignableFrom(returnType.getParameterType());
}
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
String path = request.getURI().getPath();
// Pageオブジェクトの場合
if (body instanceof Page<?> page) {
PageInfo pageInfo = PageInfo.from(page);
ApiResponse<?> apiResponse = ApiResponse.success(page.getContent(), path, pageInfo);
return handleStringConverter(apiResponse, selectedConverterType, response);
}
// 通常のオブジェクト
ApiResponse<?> apiResponse = ApiResponse.success(body, path);
return handleStringConverter(apiResponse, selectedConverterType, response);
}
private Object handleStringConverter(ApiResponse<?> apiResponse,
Class<? extends HttpMessageConverter<?>> converterType,
ServerHttpResponse response) {
// StringHttpMessageConverterが使用される場合はJSON文字列に変換
if (StringHttpMessageConverter.class.isAssignableFrom(converterType)) {
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
try {
return objectMapper.writeValueAsString(apiResponse);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to serialize response", e);
}
}
return apiResponse;
}
}
|
supports()による適用条件の制御#
supports()メソッドを活用することで、特定の条件に合致するコントローラにのみレスポンスラッパーを適用できます。
特定パッケージのコントローラのみに適用#
1
2
3
4
5
6
7
|
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
// 特定パッケージ配下のコントローラのみ対象
String packageName = returnType.getContainingClass().getPackageName();
return packageName.startsWith("com.example.demoapi.controller.api");
}
|
カスタムアノテーションによる適用制御#
特定のコントローラやメソッドを対象外にしたい場合、カスタムアノテーションを使用する方法が柔軟です。
まず、適用除外を示すアノテーションを定義します。
1
2
3
4
5
6
7
8
9
10
11
|
package com.example.demoapi.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RawResponse {
}
|
supports()でアノテーションの有無をチェックします。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
// @RawResponseが付与されている場合は対象外
if (returnType.hasMethodAnnotation(RawResponse.class)) {
return false;
}
if (returnType.getContainingClass().isAnnotationPresent(RawResponse.class)) {
return false;
}
return !ApiResponse.class.isAssignableFrom(returnType.getParameterType());
}
|
コントローラでの使用例は以下のとおりです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@RestController
@RequestMapping("/api/users")
public class UserController {
// 通常のエンドポイント(ApiResponseでラップされる)
@GetMapping("/{id}")
public UserDto getUser(@PathVariable Long id) {
return userService.findById(id);
}
// 生のレスポンスを返すエンドポイント(ラップされない)
@RawResponse
@GetMapping("/{id}/export")
public byte[] exportUser(@PathVariable Long id) {
return userService.exportToCsv(id);
}
}
|
@RestControllerAdviceのbasePackages属性による制御#
@RestControllerAdviceアノテーションの属性を使用して、適用対象を絞り込むこともできます。
1
2
3
4
5
|
// 特定パッケージ配下のコントローラのみに適用
@RestControllerAdvice(basePackages = "com.example.demoapi.controller.api")
public class ApiResponseWrapperAdvice implements ResponseBodyAdvice<Object> {
// ...
}
|
1
2
3
4
5
|
// 特定のアノテーションが付与されたコントローラのみに適用
@RestControllerAdvice(annotations = ApiController.class)
public class ApiResponseWrapperAdvice implements ResponseBodyAdvice<Object> {
// ...
}
|
1
2
3
4
5
|
// 特定のクラスまたはインターフェースを継承・実装したコントローラのみに適用
@RestControllerAdvice(assignableTypes = {BaseApiController.class})
public class ApiResponseWrapperAdvice implements ResponseBodyAdvice<Object> {
// ...
}
|
ページネーション情報の自動付与#
Spring Data JPAのPageオブジェクトを返すエンドポイントに対して、ページネーション情報を自動的に付与する実装を紹介します。
コントローラの実装例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
package com.example.demoapi.controller;
import com.example.demoapi.dto.UserDto;
import com.example.demoapi.service.UserService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public Page<UserDto> getUsers(Pageable pageable) {
return userService.findAll(pageable);
}
}
|
レスポンス例#
GET /api/users?page=0&size=10のリクエストに対して、以下のようなレスポンスが自動生成されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
{
"success": true,
"data": [
{"id": 1, "name": "田中太郎", "email": "tanaka@example.com"},
{"id": 2, "name": "佐藤花子", "email": "sato@example.com"}
],
"meta": {
"timestamp": "2026-01-04T17:00:00+09:00",
"path": "/api/users",
"page": {
"page": 0,
"size": 10,
"totalElements": 25,
"totalPages": 3,
"first": true,
"last": false
}
}
}
|
エラーレスポンスとの統合#
正常系と異常系で統一されたレスポンス形式を実現するため、@ExceptionHandlerと組み合わせます。
グローバル例外ハンドラとの連携#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
package com.example.demoapi.common.advice;
import com.example.demoapi.common.response.ApiResponse;
import com.example.demoapi.common.response.ErrorInfo;
import com.example.demoapi.exception.ResourceNotFoundException;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.List;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiResponse<Void>> handleResourceNotFound(
ResourceNotFoundException ex,
HttpServletRequest request) {
ErrorInfo errorInfo = new ErrorInfo(
"RESOURCE_NOT_FOUND",
ex.getMessage(),
null
);
ApiResponse<Void> response = ApiResponse.error(errorInfo, request.getRequestURI());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Void>> handleValidationError(
MethodArgumentNotValidException ex,
HttpServletRequest request) {
List<ErrorInfo.FieldError> fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(fe -> new ErrorInfo.FieldError(
fe.getField(),
fe.getDefaultMessage(),
fe.getRejectedValue()
))
.toList();
ErrorInfo errorInfo = new ErrorInfo(
"VALIDATION_ERROR",
"入力値が不正です",
fieldErrors
);
ApiResponse<Void> response = ApiResponse.error(errorInfo, request.getRequestURI());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleGenericException(
Exception ex,
HttpServletRequest request) {
ErrorInfo errorInfo = new ErrorInfo(
"INTERNAL_SERVER_ERROR",
"サーバー内部でエラーが発生しました",
null
);
ApiResponse<Void> response = ApiResponse.error(errorInfo, request.getRequestURI());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
}
|
エラーレスポンスの例#
1
2
3
4
5
6
7
8
9
10
11
12
|
{
"success": false,
"data": null,
"meta": {
"timestamp": "2026-01-04T17:00:00+09:00",
"path": "/api/users/999"
},
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "ユーザーが見つかりません: id=999"
}
}
|
処理フローの全体像#
以下の図は、正常系・異常系を含めたレスポンス処理の全体像を示しています。
flowchart TD
A[クライアントリクエスト] --> B[DispatcherServlet]
B --> C[Controller]
C --> D{例外発生?}
D -->|No| E[戻り値取得]
D -->|Yes| F[GlobalExceptionHandler]
F --> G[ApiResponse.error 生成]
E --> H{supports == true?}
H -->|Yes| I[ResponseBodyAdvice]
H -->|No| J[そのまま返却]
I --> K{Page型?}
K -->|Yes| L[ページ情報付与]
K -->|No| M[通常ラップ]
L --> N[ApiResponse生成]
M --> N
G --> O[HttpMessageConverter]
J --> O
N --> O
O --> P[JSONシリアライズ]
P --> Q[HTTPレスポンス]
Q --> R[クライアント]実装時の注意点#
ResponseBodyAdviceが適用されないケース#
以下のケースではResponseBodyAdviceが呼び出されません。
- void戻り値: コントローラメソッドが
voidを返す場合
- HttpServletResponse直接操作: レスポンスに直接書き込む場合
- 非同期レスポンス:
DeferredResultやCallableの内部処理
- Server-Sent Events:
SseEmitterを使用する場合
複数のResponseBodyAdvice実装がある場合#
複数のResponseBodyAdvice実装が存在する場合、@Orderアノテーションで実行順序を制御できます。
1
2
3
4
5
6
7
8
9
10
11
|
@RestControllerAdvice
@Order(1) // 値が小さいほど先に実行
public class LoggingResponseAdvice implements ResponseBodyAdvice<Object> {
// ログ出力用Advice
}
@RestControllerAdvice
@Order(2)
public class ApiResponseWrapperAdvice implements ResponseBodyAdvice<Object> {
// ラッパーAdvice
}
|
パフォーマンスへの影響#
ResponseBodyAdviceはすべてのレスポンスに対して呼び出されるため、supports()メソッドでの判定は軽量に保つことが重要です。重い処理はbeforeBodyWrite()で行い、不要な場合は早期にfalseを返すようにしましょう。
まとめ#
本記事では、ResponseBodyAdviceインターフェースを活用したREST APIレスポンスの統一化について解説しました。
| 項目 |
内容 |
| ResponseBodyAdviceの役割 |
HTTPメッセージコンバータ書き込み直前にレスポンスボディを加工 |
| supports()の活用 |
適用対象を戻り値型、パッケージ、アノテーションで制御 |
| 共通ラッパー設計 |
ApiResponseクラスで成功・エラーを統一形式でラップ |
| ページネーション対応 |
Page型を検出して自動的にページ情報を付与 |
| エラーハンドリング統合 |
@ExceptionHandlerと組み合わせて異常系も統一 |
統一されたレスポンス形式を採用することで、クライアント側の実装を簡素化し、APIの保守性と拡張性を高められます。プロジェクトの要件に応じて、本記事で紹介したパターンをカスタマイズして活用してください。
参考リンク#