HomeSpring BootSpring Boot ResponseEntity: Status, Headers and Body With Examples
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 works

Setting 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.