Spring Boot
Spring Boot ResponseEntity: Status, Headers and Body With Examples
Learn how to use ResponseEntity in Spring Boot to control the HTTP status code, headers and body: ok, created with Location, noContent, notFound, custom headers, file downloads, plus when to use it instead of @ResponseStatus.
What Is ResponseEntity?
A controller method can return a plain object, and Spring serializes it as JSON with a `200 OK` status. That is fine until you need a different status code, a `Location` or cache header, or no body at all.
`ResponseEntity<T>` represents the whole HTTP response: **status code + headers + body**. It lets a handler control all three without touching `HttpServletResponse`.
Examples target Spring Boot 3.x (Spring Framework 6).
ResponseEntity vs Other Ways to Set the Response
| Approach | Controls status | Controls headers | Dynamic at runtime | Best for |
|---|---|---|---|---|
| Return the object directly | No (always 200) | No | No | Simple GET endpoints |
| `@ResponseStatus` on the method | Yes, fixed | No | No | A single fixed status such as 201 or 204 |
| `ResponseEntity<T>` | Yes | Yes | Yes | Different statuses, `Location`, cache or custom headers |
| `ResponseStatusException` | Yes (error statuses) | Limited | Yes | Signalling errors from deep in the code |
| `@ExceptionHandler` / `@ControllerAdvice` | Yes | Yes | Yes | Consistent error responses across all controllers |
Rule of thumb: use `ResponseEntity` for **success responses that vary** (201 with Location, 204, conditional 200 or 404), and exceptions handled centrally for **errors**.
Basic Usage: Returning ResponseEntity
Compare a plain return with the three situations where `ResponseEntity` is needed: creation, empty responses and conditional results.
Java
// Plain return: always 200 OK
@GetMapping("/{id}")
public UserResponse findById(@PathVariable Long id) {
return userService.findById(id);
}
// 201 Created with a Location header
@PostMapping
public ResponseEntity<UserResponse> create(@RequestBody @Valid CreateUserRequest req) {
UserResponse created = userService.create(req);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.id())
.toUri();
return ResponseEntity.created(location).body(created);
}
// 204 No Content
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
// 200 if present, 404 if not: one line
@GetMapping("/{id}")
public ResponseEntity<UserResponse> findById(@PathVariable Long id) {
return ResponseEntity.of(userService.findOptional(id));
}Static Factory Methods and Status Cheat Sheet
`ResponseEntity` has factory methods for common statuses. Each returns a builder, so you can add headers before calling `body(...)` or `build()`.
| Method | Status | Typical use |
|---|---|---|
| `ok(body)` | 200 | Successful GET, PUT, PATCH with a body |
| `created(uri)` | 201 | POST that created a resource (sets `Location`) |
| `accepted()` | 202 | Work queued, not finished |
| `noContent()` | 204 | Successful DELETE or update with no body |
| `badRequest()` | 400 | Invalid input handled manually |
| `notFound()` | 404 | Resource doesn't exist |
| `status(HttpStatus.CONFLICT)` | any | 409, 422, 429 and other codes without a shortcut |
| `internalServerError()` | 500 | Rarely set manually; prefer exception handling |
`noContent()` and `notFound()` return a headers-only builder, so they have `build()` but no `body()`.
Java
ResponseEntity.ok().header("X-Custom", "value").body(body);
ResponseEntity.accepted().build();
ResponseEntity.badRequest().body(errorResponse);
ResponseEntity.status(HttpStatus.CONFLICT).body(errorResponse);
ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.header(HttpHeaders.RETRY_AFTER, "60")
.build();
ResponseEntity.status(422).body(errorResponse); // a raw int also worksSetting the Location Header on 201 Created
A `201 Created` response should include a `Location` header pointing to the new resource. Build the URI from the current request instead of hardcoding paths, so it stays correct if the context path or API prefix changes.
`ServletUriComponentsBuilder.fromCurrentRequest()` appends to the URL of the request being handled. Alternatively, inject `UriComponentsBuilder` as a method parameter.
Java
// Option 1: from the current request
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.id())
.toUri();
// Option 2: injected UriComponentsBuilder
@PostMapping
public ResponseEntity<UserResponse> create(
@RequestBody @Valid CreateUserRequest request,
UriComponentsBuilder uriBuilder) {
UserResponse created = userService.create(request);
URI location = uriBuilder.path("/users/{id}")
.buildAndExpand(created.id()).toUri();
return ResponseEntity.created(location).body(created);
// 201 Created
// Location: http://localhost:8080/users/43
}Adding Custom and Cache Headers
Chain `header(...)` calls before `body(...)`, or use the dedicated builder methods for common headers such as `cacheControl`, `eTag` and `contentType`. Use constants from `HttpHeaders` instead of raw strings to avoid typos.
For conditional GET with ETags across many endpoints, Spring's `ShallowEtagHeaderFilter` is often simpler than computing ETags by hand.
Java
@GetMapping("/{id}")
public ResponseEntity<UserResponse> findById(@PathVariable Long id) {
UserResponse user = userService.findById(id);
return ResponseEntity.ok()
.header("X-Request-Id", UUID.randomUUID().toString())
.body(user);
}
@GetMapping("/public/{id}")
public ResponseEntity<UserResponse> findPublic(@PathVariable Long id) {
UserResponse user = userService.findById(id);
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(Duration.ofSeconds(60)).cachePublic())
.eTag(String.valueOf(user.updatedAt().hashCode()))
.body(user);
}
// Build headers separately when there are many
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType("text/csv"));
headers.setContentDisposition(
ContentDisposition.attachment().filename("users.csv").build());
return new ResponseEntity<>(data, headers, HttpStatus.OK);ResponseEntity<Void> for Empty Responses
When there is no body, use `ResponseEntity<Void>`. It states the intent clearly and prevents accidentally serializing something.
- **DELETE** returns `204 No Content`
- **PUT or PATCH** with nothing to return uses `204`
- **Async work** accepted for later processing returns `202 Accepted`
You don't need a custom HEAD handler. Spring automatically answers HEAD requests for any `@GetMapping` method, with the same headers and no body.
Java
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build(); // 204
}
@PostMapping("/{id}/email-verification")
public ResponseEntity<Void> sendVerification(@PathVariable Long id) {
emailService.sendVerificationAsync(id);
return ResponseEntity.accepted().build(); // 202: started, not finished
}
@PutMapping("/{id}/password")
public ResponseEntity<Void> changePassword(
@PathVariable Long id,
@RequestBody @Valid ChangePasswordRequest request) {
userService.changePassword(id, request);
return ResponseEntity.noContent().build(); // 204
}Conditional Responses and Branching
When the status depends on business logic, `ResponseEntity` expresses it directly. A common case is an **upsert**: 200 if the resource existed, 201 if it was created.
Prefer `ResponseEntity.of(optional)` for "found or 404". For errors, avoid catching exceptions inside the controller and building responses by hand. Throw a domain exception and handle it once in a `@RestControllerAdvice`, so every endpoint returns errors in the same shape.
Java
// Found or 404
@GetMapping("/{id}")
public ResponseEntity<UserResponse> findById(@PathVariable Long id) {
return ResponseEntity.of(userService.findOptional(id));
}
// Upsert: 200 if updated, 201 if created
@PutMapping("/{id}")
public ResponseEntity<UserResponse> upsert(
@PathVariable Long id,
@RequestBody @Valid UpdateUserRequest request,
UriComponentsBuilder uriBuilder) {
boolean existed = userService.existsById(id);
UserResponse result = userService.upsert(id, request);
if (existed) {
return ResponseEntity.ok(result);
}
URI location = uriBuilder.path("/users/{id}").buildAndExpand(id).toUri();
return ResponseEntity.created(location).body(result);
}
// Errors: throw, and handle centrally
@PostMapping("/{id}/promote")
public UserResponse promote(@PathVariable Long id) {
return userService.promote(id); // throws AlreadyAdminException
}
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(AlreadyAdminException.class)
public ResponseEntity<ErrorResponse> handle(AlreadyAdminException ex) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(ErrorResponse.of(409, "Conflict", ex.getMessage()));
}
}Error Bodies: ErrorResponse vs ProblemDetail
You can return any object as an error body, as above. Spring Framework 6 (Spring Boot 3) also has built-in support for `ProblemDetail`, the standard RFC 9457 "problem details" format (the successor to RFC 7807). It gives clients a consistent structure (`type`, `title`, `status`, `detail`, `instance`) without a custom class.
If you start a new API on Boot 3, consider `ProblemDetail` instead of inventing your own error shape.
Java
@ExceptionHandler(AlreadyAdminException.class)
public ResponseEntity<ProblemDetail> handle(AlreadyAdminException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.CONFLICT, ex.getMessage());
problem.setTitle("User is already an admin");
return ResponseEntity.status(HttpStatus.CONFLICT).body(problem);
}File and Binary Responses
`ResponseEntity<byte[]>` suits small files held in memory. `ResponseEntity<Resource>` is better for larger files because Spring streams the resource instead of loading it all into memory. Set `Content-Type` to match the file and `Content-Disposition` to control download vs inline display.
Build `Content-Disposition` with `ContentDisposition` rather than string concatenation, because it quotes and encodes the filename correctly.
**Security:** never pass a user-supplied filename straight to the filesystem. Resolve it against a fixed base directory and reject paths that escape it, to prevent path traversal.
Java
@GetMapping("/stream/{filename}")
public ResponseEntity<Resource> stream(@PathVariable String filename) {
Resource resource = fileService.loadAsResource(filename); // validates the path
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION,
ContentDisposition.attachment()
.filename(resource.getFilename())
.build().toString())
.body(resource);
}
// Inline display (PDF opens in the browser)
@GetMapping("/invoices/{id}.pdf")
public ResponseEntity<byte[]> invoice(@PathVariable Long id) {
byte[] pdf = invoiceService.generate(id);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_PDF)
.header(HttpHeaders.CONTENT_DISPOSITION,
ContentDisposition.inline().filename("invoice-" + id + ".pdf")
.build().toString())
.body(pdf);
}Testing ResponseEntity Responses
With `MockMvc` you assert on the status, headers and body the way a client would see them. This verifies that the `Location` header and status are actually what you intended.
Java
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired private MockMvc mockMvc;
@MockitoBean private UserService userService; // @MockBean on Boot < 3.4
@Test
void create_returns201WithLocation() throws Exception {
when(userService.create(any()))
.thenReturn(new UserResponse(43L, "Alice", "alice@test.com"));
mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"Alice\",\"email\":\"alice@test.com\"}"))
.andExpect(status().isCreated())
.andExpect(header().string("Location", endsWith("/users/43")))
.andExpect(jsonPath("$.id").value(43));
}
@Test
void delete_returns204() throws Exception {
mockMvc.perform(delete("/users/1"))
.andExpect(status().isNoContent());
}
}Common Mistakes With ResponseEntity
- **Using ResponseEntity everywhere.** For a plain GET that always returns 200, returning the object directly is simpler.
- **Returning 200 with an error body.** Use the proper 4xx or 5xx status so clients and monitoring can tell it failed.
- **Calling `body()` on `noContent()`.** A 204 has no body. Use `build()`.
- **Hardcoding the Location URL.** Build it from the current request.
- **Using `ResponseEntity<?>` with try/catch in every method.** Throw exceptions and use `@RestControllerAdvice`.
- **Returning `ResponseEntity<String>` for JSON.** Return the object and let Jackson serialize it, otherwise you may double-encode.
- **Passing user filenames to the filesystem unchecked.** This allows path traversal.
- **Using a raw string for `Content-Disposition`.** Use `ContentDisposition` so special characters are handled.
When Should You Use ResponseEntity?
Use it when:
- You create a resource and must return **201 plus a Location header**
- The endpoint returns **204 or 202** with no body
- The status code **depends on runtime logic** (found vs not found, created vs updated)
- You need to set **custom, cache or content headers**
- You return **files or binary data**
Skip it when the endpoint always returns 200 with a body, or when a single fixed status is enough, in which case `@ResponseStatus` does the job.