Spring Boot REST APIを開発する際、クライアントからのリクエストデータをどのように受け取り、適切なレスポンスを返すかは非常に重要です。本記事では、@PathVariable@RequestParam@RequestBodyによるリクエストデータの取得方法と、ResponseEntityを使ったレスポンス制御について実践的なコード例とともに解説します。

実行環境と前提条件

本記事の内容を実践するにあたり、以下の環境を前提としています。

項目 バージョン・要件
Java 17以上
Spring Boot 3.4.x
ビルドツール Maven または Gradle
IDE VS Code(Extension Pack for Javaインストール済み)

また、本記事はSpring Boot REST API入門 - @RestControllerでCRUDエンドポイントを実装するの続編として、基本的なSpring Boot REST APIの知識があることを前提としています。

期待される学習成果

本記事を読み終えると、以下のことができるようになります。

  • パスパラメータとクエリパラメータを適切に使い分けてリクエストを処理できる
  • DTOパターンを使ってリクエストボディとレスポンスボディを設計できる
  • ResponseEntityを使ってHTTPステータスコードとヘッダーを制御できる

パスパラメータの受け取り方(@PathVariable)

パスパラメータは、URLパスの一部としてリソースを識別するために使用します。RESTful APIでは、特定のリソースにアクセスする際に頻繁に使用されます。

基本的な使い方

@PathVariableアノテーションを使用して、URLパスに含まれる値をメソッド引数にバインドします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package com.example.demoapi.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping("/{id}")
    public String getUser(@PathVariable Long id) {
        return "ユーザーID: " + id;
    }
}

上記のエンドポイントにGET /api/users/123でアクセスすると、id123がバインドされます。

パラメータ名の明示的な指定

パス変数名とメソッド引数名が異なる場合は、@PathVariablevalue属性で明示的に指定します。

1
2
3
4
5
6
@GetMapping("/{userId}/orders/{orderId}")
public String getUserOrder(
        @PathVariable("userId") Long userId,
        @PathVariable("orderId") Long orderId) {
    return "ユーザーID: " + userId + ", 注文ID: " + orderId;
}

必須/任意の制御

デフォルトでは、パスパラメータは必須です。任意のパスパラメータを実現したい場合は、複数のエンドポイントを定義する方法が一般的です。

1
2
3
4
5
6
7
@GetMapping({"", "/{status}"})
public String getUsers(@PathVariable(required = false) String status) {
    if (status == null) {
        return "全ユーザーを取得";
    }
    return "ステータス: " + status + " のユーザーを取得";
}

ただし、任意のパス要素が必要な場合は、クエリパラメータの使用を検討してください。

クエリパラメータの受け取り方(@RequestParam)

クエリパラメータは、URLの?以降にkey=value形式で渡されるパラメータです。フィルタリング、ソート、ページネーションなどのオプション条件を指定する際に使用します。

基本的な使い方

@RequestParamアノテーションを使用して、クエリパラメータをメソッド引数にバインドします。

1
2
3
4
@GetMapping("/search")
public String searchUsers(@RequestParam String keyword) {
    return "検索キーワード: " + keyword;
}

GET /api/users/search?keyword=tanakaでアクセスすると、keywordtanakaがバインドされます。

デフォルト値の設定

defaultValue属性を使用して、パラメータが指定されなかった場合のデフォルト値を設定できます。

1
2
3
4
5
6
7
@GetMapping
public String getUsers(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size,
        @RequestParam(defaultValue = "id") String sortBy) {
    return String.format("ページ: %d, サイズ: %d, ソート: %s", page, size, sortBy);
}

任意パラメータとOptionalの活用

required = falseを指定するか、Optional型を使用することで、任意のパラメータを定義できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@GetMapping("/filter")
public String filterUsers(
        @RequestParam(required = false) String status,
        @RequestParam Optional<String> department) {
    StringBuilder result = new StringBuilder("フィルタ条件: ");
    if (status != null) {
        result.append("ステータス=").append(status).append(" ");
    }
    department.ifPresent(d -> result.append("部署=").append(d));
    return result.toString();
}

複数値の受け取り

同じパラメータ名で複数の値を渡す場合は、Listで受け取ります。

1
2
3
4
@GetMapping("/by-ids")
public String getUsersByIds(@RequestParam List<Long> ids) {
    return "取得対象ID: " + ids;
}

GET /api/users/by-ids?ids=1&ids=2&ids=3でアクセスすると、ids[1, 2, 3]がバインドされます。

全パラメータをMapで受け取る

パラメータ名が事前に決まっていない場合や、動的なフィルタリングを実装する場合はMapで受け取ります。

1
2
3
4
@GetMapping("/dynamic-filter")
public String dynamicFilter(@RequestParam Map<String, String> params) {
    return "受け取ったパラメータ: " + params;
}

リクエストボディの受け取り方(@RequestBody)

リクエストボディは、POSTやPUTリクエストでJSONデータを送信する際に使用します。@RequestBodyアノテーションにより、JSONデータをJavaオブジェクトに自動変換(デシリアライズ)できます。

DTOクラスの設計

リクエストボディを受け取るためのDTO(Data Transfer Object)クラスを設計します。DTOはコントローラーとクライアント間のデータ交換に特化したオブジェクトです。

 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.dto;

public class UserCreateRequest {
    private String name;
    private String email;
    private Integer age;

    // デフォルトコンストラクタ(Jacksonのデシリアライズに必要)
    public UserCreateRequest() {
    }

    public UserCreateRequest(String name, String email, Integer age) {
        this.name = name;
        this.email = email;
        this.age = age;
    }

    // Getter/Setter
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

Java recordを使ったDTO定義

Java 16以降では、recordを使ってより簡潔にDTOを定義できます。

1
2
3
4
5
6
7
package com.example.demoapi.dto;

public record UserCreateRequest(
    String name,
    String email,
    Integer age
) {}

recordを使用すると、コンストラクタ、getter、equalshashCodetoStringが自動生成されます。

@RequestBodyによるDTOへのマッピング

コントローラーメソッドで@RequestBodyを使用してDTOを受け取ります。

1
2
3
4
5
6
7
8
9
@PostMapping
public String createUser(@RequestBody UserCreateRequest request) {
    return String.format(
        "ユーザー作成: 名前=%s, メール=%s, 年齢=%d",
        request.name(),
        request.email(),
        request.age()
    );
}

以下のJSONをPOSTすると、UserCreateRequestにマッピングされます。

1
2
3
4
5
{
    "name": "田中太郎",
    "email": "tanaka@example.com",
    "age": 30
}

ネストしたオブジェクトの受け取り

複雑なJSON構造も、対応するDTOを定義することで受け取れます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public record OrderCreateRequest(
    Long userId,
    List<OrderItem> items,
    ShippingAddress shippingAddress
) {}

public record OrderItem(
    Long productId,
    Integer quantity
) {}

public record ShippingAddress(
    String postalCode,
    String prefecture,
    String city,
    String addressLine
) {}
1
2
3
4
5
6
7
8
9
@PostMapping("/orders")
public String createOrder(@RequestBody OrderCreateRequest request) {
    return String.format(
        "注文作成: ユーザーID=%d, 商品数=%d, 配送先=%s",
        request.userId(),
        request.items().size(),
        request.shippingAddress().city()
    );
}

レスポンスの設計(@ResponseBodyとDTO)

@RestControllerを使用している場合、メソッドの戻り値は自動的にJSONに変換されてレスポンスボディに設定されます。これは@ResponseBodyが暗黙的に適用されているためです。

レスポンスDTOの設計

レスポンス用のDTOを設計することで、APIの出力を明確に定義できます。

1
2
3
4
5
6
7
8
package com.example.demoapi.dto;

public record UserResponse(
    Long id,
    String name,
    String email,
    String createdAt
) {}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@GetMapping("/{id}")
public UserResponse getUser(@PathVariable Long id) {
    // 実際はサービス層からデータを取得
    return new UserResponse(
        id,
        "田中太郎",
        "tanaka@example.com",
        "2026-01-04T10:00:00"
    );
}

リスト形式のレスポンス

1
2
3
4
5
6
7
@GetMapping
public List<UserResponse> getAllUsers() {
    return List.of(
        new UserResponse(1L, "田中太郎", "tanaka@example.com", "2026-01-04T10:00:00"),
        new UserResponse(2L, "山田花子", "yamada@example.com", "2026-01-04T11:00:00")
    );
}

ResponseEntityによるHTTPレスポンスの制御

ResponseEntityを使用すると、HTTPステータスコード、レスポンスヘッダー、レスポンスボディを細かく制御できます。

基本的な使い方

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
    UserResponse user = findUserById(id);
    
    if (user == null) {
        return ResponseEntity.notFound().build();
    }
    
    return ResponseEntity.ok(user);
}

ステータスコード別のファクトリメソッド

ResponseEntityクラスには、よく使うステータスコード用のファクトリメソッドが用意されています。

メソッド ステータスコード 用途
ok() 200 OK 正常なレスポンス
created(URI) 201 Created リソース作成成功
accepted() 202 Accepted 非同期処理の受付
noContent() 204 No Content 成功だがボディなし
badRequest() 400 Bad Request クライアントエラー
notFound() 404 Not Found リソースが見つからない
status(HttpStatus) 任意 カスタムステータス

リソース作成時のLocationヘッダー設定

RESTful APIでは、リソース作成時にLocationヘッダーで新しいリソースのURIを返すことが推奨されています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@PostMapping
public ResponseEntity<UserResponse> createUser(@RequestBody UserCreateRequest request) {
    // ユーザー作成処理(実際はサービス層で実行)
    Long newUserId = 100L;
    UserResponse createdUser = new UserResponse(
        newUserId,
        request.name(),
        request.email(),
        "2026-01-04T12:00:00"
    );
    
    URI location = URI.create("/api/users/" + newUserId);
    
    return ResponseEntity.created(location).body(createdUser);
}

カスタムヘッダーの追加

1
2
3
4
5
6
7
8
9
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUserWithHeaders(@PathVariable Long id) {
    UserResponse user = findUserById(id);
    
    return ResponseEntity.ok()
            .header("X-Custom-Header", "custom-value")
            .header("Cache-Control", "max-age=3600")
            .body(user);
}

削除操作の実装例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
    boolean deleted = deleteUserById(id);
    
    if (!deleted) {
        return ResponseEntity.notFound().build();
    }
    
    return ResponseEntity.noContent().build();
}

更新操作の実装例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@PutMapping("/{id}")
public ResponseEntity<UserResponse> updateUser(
        @PathVariable Long id,
        @RequestBody UserUpdateRequest request) {
    
    UserResponse existingUser = findUserById(id);
    if (existingUser == null) {
        return ResponseEntity.notFound().build();
    }
    
    // 更新処理(実際はサービス層で実行)
    UserResponse updatedUser = new UserResponse(
        id,
        request.name(),
        request.email(),
        existingUser.createdAt()
    );
    
    return ResponseEntity.ok(updatedUser);
}

実践的なCRUDコントローラーの実装例

これまでの内容を組み合わせた、実践的なコントローラーの実装例を示します。

DTOクラス一式

 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
// リクエストDTO
public record UserCreateRequest(
    String name,
    String email,
    Integer age
) {}

public record UserUpdateRequest(
    String name,
    String email,
    Integer age
) {}

// レスポンスDTO
public record UserResponse(
    Long id,
    String name,
    String email,
    Integer age,
    String createdAt,
    String updatedAt
) {}

// 一覧レスポンスDTO
public record UserListResponse(
    List<UserResponse> users,
    int totalCount,
    int page,
    int size
) {}

完全なコントローラー実装

  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
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package com.example.demoapi.controller;

import com.example.demoapi.dto.*;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/api/users")
public class UserController {

    // 仮のデータストア(実際はサービス層とリポジトリを使用)
    private final List<UserResponse> users = new ArrayList<>();
    private Long idCounter = 1L;

    // 一覧取得(ページネーション対応)
    @GetMapping
    public ResponseEntity<UserListResponse> getUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {
        
        int start = page * size;
        int end = Math.min(start + size, users.size());
        
        List<UserResponse> pageContent = users.subList(
            Math.min(start, users.size()),
            end
        );
        
        UserListResponse response = new UserListResponse(
            pageContent,
            users.size(),
            page,
            size
        );
        
        return ResponseEntity.ok(response);
    }

    // 単一取得
    @GetMapping("/{id}")
    public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
        return findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    // 検索(クエリパラメータ)
    @GetMapping("/search")
    public ResponseEntity<List<UserResponse>> searchUsers(
            @RequestParam(required = false) String name,
            @RequestParam(required = false) String email) {
        
        List<UserResponse> result = users.stream()
            .filter(u -> name == null || u.name().contains(name))
            .filter(u -> email == null || u.email().contains(email))
            .toList();
        
        return ResponseEntity.ok(result);
    }

    // 作成
    @PostMapping
    public ResponseEntity<UserResponse> createUser(
            @RequestBody UserCreateRequest request) {
        
        String now = java.time.Instant.now().toString();
        UserResponse newUser = new UserResponse(
            idCounter++,
            request.name(),
            request.email(),
            request.age(),
            now,
            now
        );
        users.add(newUser);
        
        URI location = URI.create("/api/users/" + newUser.id());
        return ResponseEntity.created(location).body(newUser);
    }

    // 更新
    @PutMapping("/{id}")
    public ResponseEntity<UserResponse> updateUser(
            @PathVariable Long id,
            @RequestBody UserUpdateRequest request) {
        
        return findById(id)
            .map(existing -> {
                String now = java.time.Instant.now().toString();
                UserResponse updated = new UserResponse(
                    id,
                    request.name(),
                    request.email(),
                    request.age(),
                    existing.createdAt(),
                    now
                );
                users.removeIf(u -> u.id().equals(id));
                users.add(updated);
                return ResponseEntity.ok(updated);
            })
            .orElse(ResponseEntity.notFound().build());
    }

    // 削除
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        boolean removed = users.removeIf(u -> u.id().equals(id));
        
        if (!removed) {
            return ResponseEntity.notFound().build();
        }
        
        return ResponseEntity.noContent().build();
    }

    private Optional<UserResponse> findById(Long id) {
        return users.stream()
            .filter(u -> u.id().equals(id))
            .findFirst();
    }
}

パスパラメータとクエリパラメータの使い分け

REST APIを設計する際、パスパラメータとクエリパラメータの使い分けは重要です。

flowchart TD
    A[パラメータの種類を決める] --> B{リソースを一意に<br>識別するか?}
    B -->|はい| C[パスパラメータ<br>@PathVariable]
    B -->|いいえ| D{フィルタ/ソート/ページ<br>などのオプションか?}
    D -->|はい| E[クエリパラメータ<br>@RequestParam]
    D -->|いいえ| F{データ本体か?}
    F -->|はい| G[リクエストボディ<br>@RequestBody]
    F -->|いいえ| E

使い分けの指針

用途 推奨
リソースの識別 パスパラメータ /users/123
サブリソースの識別 パスパラメータ /users/123/orders/456
フィルタリング クエリパラメータ /users?status=active
ソート クエリパラメータ /users?sort=name&order=asc
ページネーション クエリパラメータ /users?page=0&size=10
検索 クエリパラメータ /users/search?q=tanaka
リソースの作成/更新 リクエストボディ POST/PUTのJSONボディ

まとめ

本記事では、Spring Boot REST APIにおけるリクエストとレスポンスの処理方法について解説しました。

  • @PathVariable はURLパスからリソース識別子を取得する際に使用します
  • @RequestParam はフィルタリングやページネーションなどのオプションパラメータに使用します
  • @RequestBody はPOST/PUTリクエストのJSONボディをDTOにマッピングします
  • ResponseEntity を使用することで、ステータスコードとヘッダーを細かく制御できます
  • DTOパターンを採用することで、APIの入出力を明確に定義し、保守性を向上させることができます

次のステップとして、JacksonによるJSON処理のカスタマイズや、Bean Validationによる入力検証を学ぶことで、より堅牢なREST APIを構築できます。

参考リンク