AIによる要約
@Controllerは主にThymeleafなどの画面名を返し、@RestControllerは戻り値をレスポンス本文へ書き込みます。RestControllerはControllerとResponseBodyを組み合わせたアノテーションです。HTML画面とJSON APIをクラス単位で分け、HTTPステータスやヘッダーも返したいAPIではResponseEntityを使うと、戻り値の意図が分かりやすくなります。


Spring BootのWeb開発では、@Controllerと@RestControllerのどちらでもURLとメソッドを対応付けられます。違いは「戻り値を画面名として扱うか、レスポンス本文として扱うか」です。
アノテーションを雰囲気で選ぶと、Thymeleafのつもりで文字列を返したのにブラウザへその文字だけが表示される、APIのつもりなのにView検索エラーになる、といった不具合が起きます。役割と戻り値をセットで理解しましょう。
この記事のポイント
- HTML画面を返すクラスは
@Controllerを基本にする - JSON APIを返すクラスは
@RestControllerを基本にする - RestControllerはControllerとResponseBodyを組み合わせたもの
- 一部メソッドだけ本文を返すなら
@ResponseBodyを使える - HTTPステータスやヘッダーも表すなら
ResponseEntityを検討する
@Controllerは画面名を返す
@Controller
@RequestMapping("/users")
public class UserPageController {
@GetMapping
public String list(Model model) {
model.addAttribute("users", userService.findAll());
return "users/list";
}
}
return "users/list";は、通常templates/users/list.htmlなどのViewを表示するための論理名です。Modelへ入れたusersをThymeleafが参照し、HTMLを作ってブラウザへ返します。
入力画面、確認画面、一覧画面のようにサーバー側でHTMLを組み立てる場合は@Controllerが分かりやすい選択です。
@RestControllerは戻り値を本文へ書く
@RestController
@RequestMapping("/api/users")
public class UserApiController {
@GetMapping("/{userId}")
public UserResponse detail(@PathVariable long userId) {
return userService.findResponse(userId);
}
}
UserResponseはHttpMessageConverterによってJSONへ変換され、レスポンス本文として返されます。テンプレート名を探す処理は行いません。
{
"id": 10,
"name": "田中",
"status": "ACTIVE"
}
@RestControllerは、クラス全体へ@Controllerと@ResponseBodyを付けたのと同じ意図を表します。RESTという名前ですが、「どのようなAPI設計でも自動的にRESTfulになる」という意味ではありません。
違いを表で確認する
| 観点 | @Controller | @RestController |
|---|---|---|
| 主な用途 | HTML画面 | JSONなどのAPI |
| Stringの戻り値 | View名 | 本文の文字列 |
| オブジェクトの戻り値 | 通常はView処理を検討 | JSONなどへ変換 |
| Model | 画面へ値を渡すため使用 | 通常は使わない |
| @ResponseBody | 必要なメソッドだけ付ける | クラス全体に含まれる |
文字列がそのまま表示される原因
@RestController
public class UserController {
@GetMapping("/users")
public String list(Model model) {
model.addAttribute("users", userService.findAll());
return "users/list";
}
}
このコードではusers/listがView名ではなく、レスポンス本文になります。ブラウザにはHTML画面ではなく「users/list」という文字が表示されます。
修正はクラスを@Controllerへ変えることです。メソッドへViewResolver関連の設定を足して無理に解決するより、そのクラスが画面用かAPI用かを明確にします。
JSONのつもりなのにViewを探す原因
@Controller
public class HealthController {
@GetMapping("/api/health")
public Map<String, String> health() {
return Map.of("status", "UP");
}
}
クラスが@Controllerで、メソッドに@ResponseBodyがなければ、期待したJSON本文になりません。クラス全体がAPIなら@RestControllerへ変更します。
@Controller
public class HealthController {
@GetMapping("/api/health")
@ResponseBody
public Map<String, String> health() {
return Map.of("status", "UP");
}
}
一つの@Controllerに画面メソッドが多く、例外的に1メソッドだけ本文を返すなら@ResponseBodyでも動きます。ただし、画面とAPIが増えてきたらクラスを分けた方がURL、認証、例外処理、テストの意図が明確になります。
画面用とAPI用のControllerを分ける
@Controller
@RequestMapping("/orders")
public class OrderPageController {
// Thymeleaf画面
}
@RestController
@RequestMapping("/api/orders")
public class OrderApiController {
// JSON API
}
同じ注文機能でも、画面とAPIでは入力形式やエラーの返し方が異なります。画面はBindingResultを使って同じViewへ戻し、APIは400のJSONを返すなど、入口ごとの責務があります。
業務ロジックまで重複させる必要はありません。両方のControllerから共通のOrderServiceを呼び、画面・API固有の変換だけをControllerへ置きます。
Controllerは「同じ業務なら一つ」ではなく、「同じ入出力方式なら一つ」と考えると分けやすくなります。
ResponseEntityはいつ使うか
@PostMapping
public ResponseEntity<OrderResponse> create(
@Valid @RequestBody OrderRequest request) {
OrderResponse response = orderService.create(request);
URI location = URI.create("/api/orders/" + response.id());
return ResponseEntity
.created(location)
.body(response);
}
ResponseEntityを使うと、201 Created、Locationヘッダー、本文を一つの戻り値で表せます。ステータスが常に200で本文だけ返せばよい単純な参照APIでは、DTOを直接返しても構いません。
| 場面 | 戻り値の候補 |
|---|---|
| Thymeleaf画面 | View名のString |
| 単純な200 JSON | Response DTO |
| 201・204・ヘッダーを明示 | ResponseEntity |
| ファイルダウンロード | ResponseEntityやResource |
EntityをそのままJSONへ返さない
// 避けたい例
@GetMapping("/{orderId}")
public Order detail(@PathVariable long orderId) {
return orderRepository.findById(orderId).orElseThrow();
}
// APIの契約をDTOで表す
@GetMapping("/{orderId}")
public OrderResponse detail(@PathVariable long orderId) {
return orderService.findResponse(orderId);
}
Entityを直接返すと、DB項目の追加がAPI変更になり、遅延読み込みの関連が意図せずJSON化され、機密項目まで公開する危険があります。RestControllerだからEntityを返すのではなく、外部へ公開する項目をResponse DTOで決めます。
Content-Typeも確認する
JSON APIのレスポンスは通常application/json、HTML画面はtext/htmlです。テストではステータスだけでなく、期待するContent-Typeと本文形式も確認します。
@Test
void ユーザー詳細をJSONで返す() throws Exception {
mockMvc.perform(get("/api/users/10"))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(
MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.id").value(10));
}
現場での判断基準
- ブラウザへ完成したHTMLを返すなら@Controller
- JavaScriptや外部システムへJSONを返すなら@RestController
- 一部だけ本文を返す既存画面Controllerなら@ResponseBodyを検討する
- ステータス・ヘッダーを明示したいならResponseEntityを検討する
- 画面とAPIが混在してきたらクラスを分ける
- 業務ロジックは共通Serviceへ置く
現場レビューでよくある指摘
// レビューコメント例 ThymeleafのView名を返していますが、クラスが@RestControllerです。 画面用なら@Controllerへ変更してください。 // レビューコメント例 画面表示とJSON APIが同じControllerに混在しています。 URL・例外処理・テストの責務を分けるためクラスを分割してください。 // レビューコメント例 JPA Entityをそのままレスポンスへ返しています。 公開項目をResponse DTOで定義し、遅延関連や内部項目を出さないようにしてください。
提出前のセルフチェック
- 戻り値をView名と本文のどちらとして扱うか明確か
- 画面には@Controller、APIには@RestControllerを選んだか
- 画面用とAPI用のクラスが混在していないか
- APIでEntityを直接返していないか
- 必要なHTTPステータスとヘッダーを表せているか
- Content-Typeをテストしたか
- 業務ロジックがControllerへ重複していないか
WebとHTTPの土台を学ぶ参考書
Controllerのアノテーションだけでなく、HTTPメソッド、ステータス、ヘッダー、リソース設計を理解すると、画面とAPIの戻り値を判断しやすくなります。
Webを支える技術
HTTP・URI・RESTを、Webアプリ開発の土台から理解する。
RequestParamやRequestBodyの暗記だけでは見えにくい、Web通信の基本原則を整理できます。
- HTTPの仕組みから理解したい
- RESTやURIの設計意図を学びたい
当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。
この記事とあわせて読みたい
まとめ
@ControllerはView名を返す画面処理、@RestControllerは戻り値をJSONなどのレスポンス本文へ書くAPI処理に向いています。RestControllerにはResponseBodyの役割が含まれます。
画面とAPIをクラス単位で分け、共通の業務処理はServiceへ置きます。戻り値、Content-Type、ステータスをテストし、Entityではなく公開用DTOでAPIの契約を表しましょう。
