ResponseEntity & Status Codes
Returning a plain object from a handler always yields 200 OK. When you need to set a different status, add headers, or return an empty body, wrap the result in a ResponseEntity. This page covers its fluent builder, status-code selection, headers (including Location), and the lighter-weight @ResponseStatus.
Why ResponseEntity
ResponseEntity<T> represents the entire HTTP response — status line, headers, and body. It gives full control while still serializing the body through Jackson.
@GetMapping("/{id}")
public ResponseEntity<Product> one(@PathVariable Long id) {
return service.findById(id)
.map(ResponseEntity::ok) // 200 with body
.orElse(ResponseEntity.notFound().build()); // 404 no body
}
The builder API
ResponseEntity exposes static factory methods and a fluent builder.
// 200 OK with a body
return ResponseEntity.ok(product);
// 201 Created with a body
return ResponseEntity.status(HttpStatus.CREATED).body(product);
// 204 No Content (empty body)
return ResponseEntity.noContent().build();
// 404 Not Found
return ResponseEntity.notFound().build();
// Custom status and headers
return ResponseEntity.status(HttpStatus.ACCEPTED)
.header("X-Request-Id", requestId)
.body(result);
build() finishes a response with no body; body(...) finishes one with a payload.
Status code reference
| Status | Constant | When to use |
|---|---|---|
| 200 | OK | Successful GET/PUT/PATCH with a body |
| 201 | CREATED | Resource created (return it + Location) |
| 202 | ACCEPTED | Async work accepted, not yet done |
| 204 | NO_CONTENT | Success with no body (DELETE) |
| 400 | BAD_REQUEST | Malformed input / validation failure |
| 401 | UNAUTHORIZED | Missing/invalid authentication |
| 403 | FORBIDDEN | Authenticated but not allowed |
| 404 | NOT_FOUND | Resource does not exist |
| 409 | CONFLICT | State conflict (duplicate, version) |
| 422 | UNPROCESSABLE_ENTITY | Semantically invalid payload |
| 500 | INTERNAL_SERVER_ERROR | Unhandled server error |
Location header on create
The REST convention for POST is to return 201 Created plus a Location header pointing at the new resource. Build the URI from the current request to avoid hard-coding the host.
@PostMapping
public ResponseEntity<Product> create(@Valid @RequestBody ProductRequest body) {
Product saved = service.create(body);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(saved.getId())
.toUri();
return ResponseEntity.created(location).body(saved);
}
Request:
curl -i -X POST http://localhost:8080/api/products \
-H "Content-Type: application/json" \
-d '{"name":"Keyboard","price":59.99}'
Output:
HTTP/1.1 201 Created
Location: http://localhost:8080/api/products/101
Content-Type: application/json
{ "id": 101, "name": "Keyboard", "price": 59.99 }
Setting headers
Add headers fluently, or build an HttpHeaders object for several at once.
return ResponseEntity.ok()
.header("Cache-Control", "max-age=60")
.eTag("\"v3\"")
.body(product);
@ResponseStatus
When a handler (or exception) always maps to a fixed status and you do not need the builder, @ResponseStatus is more concise. Spring applies it to the response automatically.
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Product create(@Valid @RequestBody ProductRequest body) {
return service.create(body);
}
It shines on custom exceptions — throwing one yields the declared status without any ResponseEntity plumbing:
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(Long id) {
super("Product " + id + " not found");
}
}
@GetMapping("/{id}")
public Product one(@PathVariable Long id) {
return service.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}
Output:
HTTP/1.1 404 Not Found
Note: Use
ResponseEntitywhen status/headers vary per request (e.g. 200 vs 404). Use@ResponseStatuswhen the status is constant for the handler or exception type. For structured error bodies, prefer Problem Detail.
ResponseEntity vs @ResponseStatus
| Aspect | ResponseEntity | @ResponseStatus |
|---|---|---|
| Status | Dynamic, per call | Fixed, declared |
| Headers | Full control | None |
| Body | Any, optional | Method return value |
| Best for | Branching responses | Constant status, exceptions |
Pitfalls
- Returning
nullfrom a handler gives200 OKwith an empty body, rarely what you want — usenotFound()explicitly. - Do not hard-code host/port in
Location; build it from the request withServletUriComponentsBuilder. @ResponseStatuson a handler that also returns aResponseEntityis ignored — the entity wins.