HTTP-методы и статусы: ResponseEntity и @ResponseStatus

Хорошее REST API говорит статусами: 200 — успех, 201 — создано, 204 — без содержимого, 404 — не найдено. Spring даёт два инструмента управления ими.
Суть: @ResponseStatus фиксирует статус метода декларативно, а ResponseEntity даёт полный программный контроль над статусом, заголовками и телом.

HTTP-статус — это первое, что читает клиент. Если ваш API на любую операцию возвращает 200, клиент не сможет отличить успех создания от ошибки. Грамотные статусы делают API предсказуемым и самодокументируемым.

Семантика основных кодов

КодЗначениеКогда возвращать
200 OKУспехУспешный GET, PUT
201 CreatedСозданоУспешный POST
204 No ContentБез телаУспешный DELETE
400 Bad RequestОшибка клиентаНевалидные данные
404 Not FoundНе найденоРесурс отсутствует

ResponseEntity — полный контроль

@PostMapping("/api/users")
public ResponseEntity<User> create(@RequestBody CreateUserRequest req) {
    User saved = service.create(req);
    return ResponseEntity
            .status(HttpStatus.CREATED)        // 201
            .header("Location", "/api/users/" + saved.getId())
            .body(saved);
}

@GetMapping("/{id}")
public ResponseEntity<User> byId(@PathVariable Long id) {
    return service.findOptional(id)
            .map(ResponseEntity::ok)           // 200 + тело
            .orElse(ResponseEntity.notFound().build()); // 404
}

ResponseEntity — обёртка над всем ответом: статус, заголовки, тело. Это самый гибкий способ, особенно когда статус зависит от условия (нашли — 200, нет — 404).

@ResponseStatus — декларативно

@PostMapping("/api/users")
@ResponseStatus(HttpStatus.CREATED)            // всегда 201
public User create(@RequestBody CreateUserRequest req) {
    return service.create(req);
}

Если статус фиксированный, @ResponseStatus короче и читается лучше.

Как работает под капотом

Когда метод возвращает обычный объект, Spring оборачивает его в ответ со статусом 200 (или тем, что указан в @ResponseStatus). Когда возвращается ResponseEntity, Spring берёт статус, заголовки и тело прямо из него, не добавляя своих умолчаний.

  Метод контроллера
        |
   ┌────┴─────────────────────────┐
   v                              v
 обычный объект              ResponseEntity
   |                              |
 статус = 200                 статус/заголовки/тело
 (или @ResponseStatus)        берутся из объекта
   |                              |
   └──────────────┬───────────────┘
                  v
          HTTP-ответ клиенту

Частые ошибки

  • POST возвращает 200 вместо 201. Создание ресурса должно отвечать 201 Created.
  • 404 как 200 с пустым телом. Если ресурса нет, верните 404, а не «успех» с null.
  • Ошибки как 200. Бизнес-ошибки должны отражаться в статусах 4xx, а не маскироваться под успех.

Best practices

  • Используйте @ResponseStatus для фиксированного статуса, ResponseEntity — когда статус зависит от логики.
  • На создание возвращайте 201 и заголовок Location с адресом нового ресурса.
  • На удаление — 204 без тела.

Итог: статусы — язык REST. ResponseEntity даёт полный контроль, @ResponseStatus — лаконичность для фиксированных кодов. Возвращайте честные статусы — это основа удобного API.

Закрепим главное

Грамотные HTTP-статусы — признак зрелого API. Клиентский код полагается на них: фронтенд по статусу 201 понимает, что создание прошло, по 404 показывает «не найдено», по 400 — выводит ошибки формы. Если же всё возвращает 200, клиенту приходится разбирать тело ответа вручную, и логика расползается. Поэтому относитесь к статусу как к части контракта, а не как к мелочи.

Запомните практическое правило выбора инструмента. Когда статус метода фиксирован и не зависит от данных, лаконичнее @ResponseStatus — он сразу виден над методом. Когда же ответ ветвится (нашли ресурс — 200, не нашли — 404, создали — 201 с заголовком Location), нужен ResponseEntity, дающий полный программный контроль. Не пытайтесь выразить ветвление через @ResponseStatus — получится неуклюже. И всегда возвращайте заголовок Location при создании ресурса: это часть соглашений REST, по которой клиент узнаёт адрес новой сущности.

Проверьте себя
1. Какой HTTP-статус правильно вернуть на успешный POST, создавший ресурс?
A200 OK
B201 Created
C204 No Content
D302 Found
2. Когда стоит выбрать ResponseEntity вместо @ResponseStatus?
AВсегда, @ResponseStatus устарел
BКогда статус и/или заголовки зависят от условий (нашли — 200, нет — 404)
CТолько для GET-запросов
DКогда нужно вернуть строку