Spring BootのControllerとRestControllerの違いを解説する記事のアイキャッチ。Java道場風の空間で若手エンジニアがHTML画面とJSON APIを学ぶイラスト。

Spring

Spring Bootの@Controllerと@RestControllerの違い|画面とJSONで使い分ける

AIによる要約

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

新人SE
新人SE
Controllerから"users/list"をreturnしたのに、画面ではなく文字列がそのまま表示されました。
ポンコツSE
ポンコツSE
クラスに@RestControllerが付いていないか確認してください。その戻り値はテンプレート名ではなくレスポンス本文として扱われます。

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 JSONResponse 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));
}

現場での判断基準

  1. ブラウザへ完成したHTMLを返すなら@Controller
  2. JavaScriptや外部システムへJSONを返すなら@RestController
  3. 一部だけ本文を返す既存画面Controllerなら@ResponseBodyを検討する
  4. ステータス・ヘッダーを明示したいならResponseEntityを検討する
  5. 画面とAPIが混在してきたらクラスを分ける
  6. 業務ロジックは共通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を支える技術」の表紙
商品画像:Amazon.co.jp
PR Web開発に入る新人向け

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の契約を表しましょう。

-Spring
-, , , ,