Spring BootResponseEntity
Spring Boot

ResponseEntity

ResponseEntity<T> gives you full control over the HTTP response returned from a controller handler — status code, headers, and body in a single return value. It is the standard return type for REST controllers that need to set the Location header on creation, return 204 No Content, add custom headers, or choose between multiple status codes based on business logic.

Why ResponseEntity

A controller method can return a plain object — Spring serializes it with a 200 OK response. But many REST operations require a different status code, a Location header, or no body at all. ResponseEntity<T> wraps the response body with an explicit status code and header map, giving the handler full control without coupling to HttpServletResponse. The three things ResponseEntity controls: the HTTP status code (200, 201, 204, 404, etc.), the response headers (Location, Cache-Control, ETag, custom headers), and the response body (a serialized object, empty, or a stream).
Java
// ── Without ResponseEntity — always 200 OK, no header control ─────────
@GetMapping("/{id}")
public UserResponse findById(@PathVariable Long id) {
    return userService.findById(id);   // 200 OK, body serialized to JSON
}

// ── With ResponseEntity — full control ─────────────────────────────────
@GetMapping("/{id}")
public ResponseEntity<UserResponse> findById(@PathVariable Long id) {
    UserResponse user = userService.findById(id);
    return ResponseEntity.ok(user);    // explicit 200 OK
}

// ── When ResponseEntity is needed ─────────────────────────────────────
// 1. Creation — 201 Created + Location header:
@PostMapping
public ResponseEntity<UserResponse> create(@RequestBody @Valid CreateUserRequest req) {
    UserResponse created = userService.create(req);
    URI location = URI.create("/users/" + created.id());
    return ResponseEntity.created(location).body(created);
}

// 2. No body — 204 No Content:
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
    userService.delete(id);
    return ResponseEntity.noContent().build();
}

// 3. Conditional — different status based on outcome:
@PostMapping("/{id}/activate")
public ResponseEntity<UserResponse> activate(@PathVariable Long id) {
    Optional<UserResponse> result = userService.activate(id);
    return result
        .map(ResponseEntity::ok)
        .orElse(ResponseEntity.notFound().build());
}

Static Factory Methods

ResponseEntity provides static factory methods for the most common status codes. Each returns a builder that accepts headers and a body before building the final response.
Java
// ── 200 OK ────────────────────────────────────────────────────────────
ResponseEntity.ok(body)                         // 200 + body
ResponseEntity.ok().build()                     // 200 + no body
ResponseEntity.ok().header("X-Custom", "value").body(body)

// ── 201 Created ───────────────────────────────────────────────────────
ResponseEntity.created(locationUri).build()     // 201 + Location header, no body
ResponseEntity.created(locationUri).body(body)  // 201 + Location header + body

// ── 202 Accepted ──────────────────────────────────────────────────────
ResponseEntity.accepted().build()               // 202 + no body
ResponseEntity.accepted().body(body)            // 202 + body

// ── 204 No Content ────────────────────────────────────────────────────
ResponseEntity.noContent().build()              // 204, no body allowed

// ── 400 Bad Request ───────────────────────────────────────────────────
ResponseEntity.badRequest().build()
ResponseEntity.badRequest().body(errorResponse)

// ── 404 Not Found ─────────────────────────────────────────────────────
ResponseEntity.notFound().build()               // 404, no body

// ── 500 Internal Server Error ─────────────────────────────────────────
ResponseEntity.internalServerError().build()
ResponseEntity.internalServerError().body(errorResponse)

// ── Arbitrary status code ─────────────────────────────────────────────
ResponseEntity.status(HttpStatus.CONFLICT).body(errorResponse)
ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
    .header("Retry-After", "60")
    .build()
ResponseEntity.status(422).body(errorResponse)  // raw int also accepted

Builder API — Headers and Body

The builder pattern chains header additions onto the status factory before calling body() or build(). Headers can be added one at a time or from an HttpHeaders instance.
Java
@RestController
@RequestMapping("/users")
public class UserController {

    // ── Location header on creation ────────────────────────────────────
    @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: /users/43
        // Content-Type: application/json
        // { "id": 43, "name": "Alice", ... }
    }

    // ── Custom headers ─────────────────────────────────────────────────
    @GetMapping("/{id}")
    public ResponseEntity<UserResponse> findById(@PathVariable Long id) {
        UserResponse user = userService.findById(id);
        return ResponseEntity.ok()
            .header("X-User-Role", user.role().name())
            .header("X-Request-Id", UUID.randomUUID().toString())
            .body(user);
    }

    // ── Cache headers ──────────────────────────────────────────────────
    @GetMapping("/public/{id}")
    public ResponseEntity<UserResponse> findPublic(@PathVariable Long id) {
        UserResponse user = userService.findById(id);
        return ResponseEntity.ok()
            .cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS).cachePublic())
            .eTag(String.valueOf(user.updatedAt().hashCode()))
            .body(user);
    }

    // ── HttpHeaders object — build headers separately ──────────────────
    @GetMapping("/export")
    public ResponseEntity<byte[]> export() {
        byte[] data = reportService.generateCsv();

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.TEXT_PLAIN);
        headers.setContentDispositionFormData("attachment", "users.csv");
        headers.setContentLength(data.length);

        return new ResponseEntity<>(data, headers, HttpStatus.OK);
    }
}

ResponseEntity<Void> for Empty Responses

When the response has no body — DELETE, some PUT/PATCH operations, and async acceptance — ResponseEntity<Void> signals intent clearly and prevents accidental body serialization.
Java
@RestController
@RequestMapping("/users")
public class UserController {

    // ── DELETE — 204 No Content ────────────────────────────────────────
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        userService.delete(id);
        return ResponseEntity.noContent().build();
    }

    // ── Async acceptance — 202 Accepted ───────────────────────────────
    @PostMapping("/{id}/email-verification")
    public ResponseEntity<Void> sendVerification(@PathVariable Long id) {
        emailService.sendVerificationAsync(id);   // fire and forget
        return ResponseEntity.accepted().build();
        // 202 Accepted — work started, not yet complete
    }

    // ── PUT with no response body ──────────────────────────────────────
    @PutMapping("/{id}/password")
    public ResponseEntity<Void> changePassword(
            @PathVariable Long id,
            @RequestBody @Valid ChangePasswordRequest request) {
        userService.changePassword(id, request);
        return ResponseEntity.noContent().build();
        // 204 No Content — success, nothing to return
    }

    // ── HEAD — same as GET but no body ────────────────────────────────
    @RequestMapping(value = "/{id}", method = RequestMethod.HEAD)
    public ResponseEntity<Void> exists(@PathVariable Long id) {
        userService.findById(id);   // throws 404 if not found
        return ResponseEntity.ok().build();
        // 200 OK + headers, no body (Spring strips body for HEAD automatically)
    }
}

Conditional Responses and Branching

Handlers that may return different status codes based on business logic — found vs not found, created vs existing, sync vs async — express this cleanly with ResponseEntity and a wildcard generic when the body type varies.
Java
@RestController
@RequestMapping("/users")
public class UserController {

    // ── Conditional — found or not found ──────────────────────────────
    @GetMapping("/{id}")
    public ResponseEntity<UserResponse> findById(@PathVariable Long id) {
        return userService.findOptional(id)
            .map(ResponseEntity::ok)                    // 200 + body
            .orElse(ResponseEntity.notFound().build()); // 404 + no body
    }

    // ── Upsert — created or updated ───────────────────────────────────
    @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);                  // 200 — updated
        } else {
            URI location = uriBuilder.path("/users/{id}")
                .buildAndExpand(id).toUri();
            return ResponseEntity.created(location).body(result); // 201 — created
        }
    }

    // ── Mixed body types — ResponseEntity<?> ──────────────────────────
    @PostMapping("/{id}/promote")
    public ResponseEntity<?> promote(@PathVariable Long id) {
        try {
            UserResponse promoted = userService.promote(id);
            return ResponseEntity.ok(promoted);
        } catch (AlreadyAdminException ex) {
            return ResponseEntity.status(HttpStatus.CONFLICT)
                .body(ErrorResponse.of(409, "Conflict", ex.getMessage()));
        }
    }

    // ── Rate limit response ────────────────────────────────────────────
    @PostMapping("/bulk-email")
    public ResponseEntity<Void> bulkEmail(
            @RequestBody @Valid BulkEmailRequest request) {
        if (rateLimiter.isExceeded()) {
            return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
                .header("Retry-After", "60")
                .build();
        }
        emailService.sendBulk(request);
        return ResponseEntity.accepted().build();
    }
}

File and Binary Responses

ResponseEntity<byte[]> or ResponseEntity<Resource> handles file downloads and binary responses. Set Content-Disposition to trigger a browser download and Content-Type to match the file format.
Java
@RestController
@RequestMapping("/files")
public class FileController {

    // ── byte[] — small files loaded fully into memory ──────────────────
    @GetMapping("/{filename}")
    public ResponseEntity<byte[]> download(@PathVariable String filename) {
        byte[] data = fileService.load(filename);
        String contentType = fileService.detectContentType(filename);

        return ResponseEntity.ok()
            .contentType(MediaType.parseMediaType(contentType))
            .contentLength(data.length)
            .header(HttpHeaders.CONTENT_DISPOSITION,
                "attachment; filename="" + filename + """)
            .body(data);
    }

    // ── Resource — streaming, efficient for large files ────────────────
    @GetMapping("/stream/{filename}")
    public ResponseEntity<Resource> stream(@PathVariable String filename) {
        Resource resource = fileService.loadAsResource(filename);

        return ResponseEntity.ok()
            .contentType(MediaType.APPLICATION_OCTET_STREAM)
            .header(HttpHeaders.CONTENT_DISPOSITION,
                "attachment; filename="" + resource.getFilename() + """)
            .body(resource);
        // Spring streams the Resource without loading it fully into memory
    }

    // ── Inline display (PDF in 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,
                "inline; filename="invoice-" + id + ".pdf"")
            .body(pdf);
        // "inline" tells the browser to display rather than download
    }
}