ResponseEntity in Spring Boot
Table of Contents
- What is ResponseEntity?
- Theory and Concepts
- Basic Usage
- ResponseEntity Methods
- Working with DTOs
- HTTP Status Codes
- Headers Management
- Best Practices
- Real-world Examples
What is ResponseEntity?
ResponseEntity
is a Spring Framework class that represents the entire HTTP response including the status code, headers, and body. It provides fine-grained control over the HTTP response in Spring Boot REST APIs.
Key Features:
- Complete HTTP Response Control: Status code, headers, and body
- Type Safety: Generic type support for response body
- Flexible: Can be used with or without response body
- Builder Pattern: Fluent API for easy construction
Theory and Concepts
HTTP Response Structure
HTTP/1.1 200 OK ← Status Line
Content-Type: application/json ← Headers
Content-Length: 85
Cache-Control: no-cache
{ ← Body
"id": 1,
"name": "John Doe"
}
ResponseEntity Anatomy
ResponseEntity<T> = Status Code + Headers + Body
- Status Code: HTTP status (200, 404, 500, etc.)
- Headers: HTTP headers (Content-Type, Authorization, etc.)
- Body: Response payload (JSON, XML, plain text, etc.)
Basic Usage
Simple ResponseEntity
@RestController
public class UserController {
// Basic usage - just status
@GetMapping("/health")
public ResponseEntity<String> health() {
return ResponseEntity.ok("Service is running");
}
// With custom status
@PostMapping("/users")
public ResponseEntity<String> createUser() {
// Business logic here
return ResponseEntity.status(HttpStatus.CREATED)
.body("User created successfully");
}
// No content response
@DeleteMapping("/users/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
// Delete logic here
return ResponseEntity.noContent().build();
}
}
Constructor vs Builder Pattern
// Constructor approach
return new ResponseEntity<>("Hello World", HttpStatus.OK);
// Builder pattern (Recommended)
return ResponseEntity.ok()
.header("Custom-Header", "value")
.body("Hello World");
ResponseEntity Methods
Static Factory Methods
Success Responses
// 200 OK
ResponseEntity.ok()
ResponseEntity.ok("body")
ResponseEntity.ok().body(object)
// 201 Created
ResponseEntity.status(HttpStatus.CREATED)
ResponseEntity.created(uri)
// 204 No Content
ResponseEntity.noContent()
Error Responses
// 400 Bad Request
ResponseEntity.badRequest()
ResponseEntity.badRequest().body("Invalid input")
// 404 Not Found
ResponseEntity.notFound()
// 500 Internal Server Error
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
Generic Status
ResponseEntity.status(HttpStatus.ACCEPTED)
ResponseEntity.status(202) // Status code as integer
Builder Methods
ResponseEntity.ok()
.header("X-Custom-Header", "value")
.contentType(MediaType.APPLICATION_JSON)
.cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS))
.body(responseBody);
Working with DTOs
DTO Classes
// User DTO
public class UserDTO {
private Long id;
private String name;
private String email;
private LocalDateTime createdAt;
// Constructors
public UserDTO() {}
public UserDTO(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
this.createdAt = LocalDateTime.now();
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}
// Request DTO
public class CreateUserRequestDTO {
@NotBlank(message = "Name is required")
private String name;
@Email(message = "Invalid email format")
@NotBlank(message = "Email is required")
private String email;
// Getters and Setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
// Response Wrapper DTO
public class ApiResponseDTO<T> {
private boolean success;
private String message;
private T data;
private LocalDateTime timestamp;
public ApiResponseDTO(boolean success, String message, T data) {
this.success = success;
this.message = message;
this.data = data;
this.timestamp = LocalDateTime.now();
}
// Static factory methods
public static <T> ApiResponseDTO<T> success(T data) {
return new ApiResponseDTO<>(true, "Success", data);
}
public static <T> ApiResponseDTO<T> success(String message, T data) {
return new ApiResponseDTO<>(true, message, data);
}
public static <T> ApiResponseDTO<T> error(String message) {
return new ApiResponseDTO<>(false, message, null);
}
// Getters and Setters
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public T getData() { return data; }
public void setData(T data) { this.data = data; }
public LocalDateTime getTimestamp() { return timestamp; }
public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
}
Controller with DTOs
@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {
@Autowired
private UserService userService;
// GET - Single User
@GetMapping("/{id}")
public ResponseEntity<ApiResponseDTO<UserDTO>> getUserById(@PathVariable Long id) {
try {
UserDTO user = userService.findById(id);
if (user != null) {
return ResponseEntity.ok(ApiResponseDTO.success(user));
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponseDTO.error("User not found"));
}
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponseDTO.error("Internal server error"));
}
}
// GET - All Users with Pagination
@GetMapping
public ResponseEntity<ApiResponseDTO<List<UserDTO>>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
try {
List<UserDTO> users = userService.findAll(page, size);
return ResponseEntity.ok(ApiResponseDTO.success("Users retrieved successfully", users));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponseDTO.error("Failed to retrieve users"));
}
}
// POST - Create User
@PostMapping
public ResponseEntity<ApiResponseDTO<UserDTO>> createUser(
@Valid @RequestBody CreateUserRequestDTO request) {
try {
UserDTO createdUser = userService.createUser(request);
URI location = URI.create("/api/users/" + createdUser.getId());
return ResponseEntity.created(location)
.header("X-Created-Resource", "User")
.body(ApiResponseDTO.success("User created successfully", createdUser));
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest()
.body(ApiResponseDTO.error("Invalid input: " + e.getMessage()));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponseDTO.error("Failed to create user"));
}
}
// PUT - Update User
@PutMapping("/{id}")
public ResponseEntity<ApiResponseDTO<UserDTO>> updateUser(
@PathVariable Long id,
@Valid @RequestBody CreateUserRequestDTO request) {
try {
UserDTO updatedUser = userService.updateUser(id, request);
if (updatedUser != null) {
return ResponseEntity.ok(ApiResponseDTO.success("User updated successfully", updatedUser));
} else {
return ResponseEntity.notFound().build();
}
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest()
.body(ApiResponseDTO.error("Invalid input: " + e.getMessage()));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponseDTO.error("Failed to update user"));
}
}
// DELETE - Delete User
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponseDTO<Void>> deleteUser(@PathVariable Long id) {
try {
boolean deleted = userService.deleteUser(id);
if (deleted) {
return ResponseEntity.ok(ApiResponseDTO.success("User deleted successfully", null));
} else {
return ResponseEntity.notFound().build();
}
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponseDTO.error("Failed to delete user"));
}
}
}
HTTP Status Codes
Common Status Codes in Spring Boot
2xx Success
// 200 OK - Request successful
ResponseEntity.ok(data)
// 201 Created - Resource created
ResponseEntity.status(HttpStatus.CREATED).body(data)
// 204 No Content - Success but no content to return
ResponseEntity.noContent().build()
// 202 Accepted - Request accepted for processing
ResponseEntity.accepted().build()
4xx Client Errors
// 400 Bad Request - Invalid request
ResponseEntity.badRequest().body("Invalid data")
// 401 Unauthorized - Authentication required
ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Authentication required")
// 403 Forbidden - Access denied
ResponseEntity.status(HttpStatus.FORBIDDEN).body("Access denied")
// 404 Not Found - Resource not found
ResponseEntity.notFound().build()
// 409 Conflict - Resource conflict
ResponseEntity.status(HttpStatus.CONFLICT).body("Resource already exists")
// 422 Unprocessable Entity - Validation errors
ResponseEntity.unprocessableEntity().body(validationErrors)
5xx Server Errors
// 500 Internal Server Error
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Server error")
// 503 Service Unavailable
ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body("Service temporarily unavailable")
Headers Management
Common Headers
@GetMapping("/users/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
UserDTO user = userService.findById(id);
return ResponseEntity.ok()
.header("X-Total-Count", "1")
.header("X-Request-ID", UUID.randomUUID().toString())
.contentType(MediaType.APPLICATION_JSON)
.cacheControl(CacheControl.maxAge(300, TimeUnit.SECONDS))
.lastModified(user.getLastModified())
.eTag(user.getVersion().toString())
.body(user);
}
// Multiple headers
@GetMapping("/users")
public ResponseEntity<List<UserDTO>> getUsers() {
List<UserDTO> users = userService.findAll();
HttpHeaders headers = new HttpHeaders();
headers.add("X-Total-Count", String.valueOf(users.size()));
headers.add("X-Page-Number", "1");
headers.add("X-Page-Size", "10");
headers.add("Access-Control-Allow-Origin", "*");
return ResponseEntity.ok()
.headers(headers)
.body(users);
}
CORS Headers
@CrossOrigin(origins = "http://localhost:3000")
@GetMapping("/users")
public ResponseEntity<List<UserDTO>> getUsers() {
List<UserDTO> users = userService.findAll();
return ResponseEntity.ok()
.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
.header("Access-Control-Allow-Headers", "Content-Type, Authorization")
.body(users);
}
Best Practices
1. Consistent Response Structure
// Always use a consistent response wrapper
public class ApiResponse<T> {
private boolean success;
private String message;
private T data;
private Map<String, Object> metadata;
// Methods...
}
2. Proper Error Handling
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ApiResponseDTO<Void>> handleEntityNotFound(EntityNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponseDTO.error(ex.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponseDTO<Map<String, String>>> handleValidation(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
return ResponseEntity.badRequest()
.body(ApiResponseDTO.error("Validation failed").setData(errors));
}
}
3. Use Appropriate HTTP Methods and Status Codes
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}") // 200 OK or 404 NOT_FOUND
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) { /* ... */ }
@PostMapping // 201 CREATED
public ResponseEntity<UserDTO> createUser(@RequestBody UserDTO user) { /* ... */ }
@PutMapping("/{id}") // 200 OK or 404 NOT_FOUND
public ResponseEntity<UserDTO> updateUser(@PathVariable Long id, @RequestBody UserDTO user) { /* ... */ }
@DeleteMapping("/{id}") // 204 NO_CONTENT or 404 NOT_FOUND
public ResponseEntity<Void> deleteUser(@PathVariable Long id) { /* ... */ }
}
4. Location Header for Created Resources
@PostMapping
public ResponseEntity<UserDTO> createUser(@RequestBody CreateUserRequestDTO request) {
UserDTO createdUser = userService.createUser(request);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(createdUser.getId())
.toUri();
return ResponseEntity.created(location).body(createdUser);
}
Real-world Examples
E-commerce Product API
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping
public ResponseEntity<PageResponseDTO<ProductDTO>> getProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String category,
@RequestParam(required = false) String sortBy) {
PageResponseDTO<ProductDTO> products = productService.getProducts(page, size, category, sortBy);
return ResponseEntity.ok()
.header("X-Total-Elements", String.valueOf(products.getTotalElements()))
.header("X-Total-Pages", String.valueOf(products.getTotalPages()))
.cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS))
.body(products);
}
@PostMapping
public ResponseEntity<ApiResponseDTO<ProductDTO>> createProduct(
@Valid @RequestBody CreateProductRequestDTO request,
HttpServletRequest httpRequest) {
try {
ProductDTO product = productService.createProduct(request);
String location = httpRequest.getRequestURL().toString() + "/" + product.getId();
return ResponseEntity.status(HttpStatus.CREATED)
.header("Location", location)
.header("X-Resource-ID", product.getId().toString())
.body(ApiResponseDTO.success("Product created successfully", product));
} catch (ProductAlreadyExistsException e) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(ApiResponseDTO.error("Product with this SKU already exists"));
}
}
@PatchMapping("/{id}/stock")
public ResponseEntity<ApiResponseDTO<ProductDTO>> updateStock(
@PathVariable Long id,
@RequestBody UpdateStockRequestDTO request) {
try {
ProductDTO updatedProduct = productService.updateStock(id, request.getQuantity());
return ResponseEntity.ok()
.header("X-Stock-Updated", "true")
.body(ApiResponseDTO.success("Stock updated successfully", updatedProduct));
} catch (InsufficientStockException e) {
return ResponseEntity.badRequest()
.body(ApiResponseDTO.error("Insufficient stock available"));
} catch (ProductNotFoundException e) {
return ResponseEntity.notFound().build();
}
}
}
File Upload/Download API
@RestController
@RequestMapping("/api/files")
public class FileController {
@PostMapping("/upload")
public ResponseEntity<ApiResponseDTO<FileDTO>> uploadFile(
@RequestParam("file") MultipartFile file) {
try {
if (file.isEmpty()) {
return ResponseEntity.badRequest()
.body(ApiResponseDTO.error("File cannot be empty"));
}
FileDTO uploadedFile = fileService.uploadFile(file);
return ResponseEntity.status(HttpStatus.CREATED)
.header("X-File-Size", String.valueOf(file.getSize()))
.header("X-File-Type", file.getContentType())
.body(ApiResponseDTO.success("File uploaded successfully", uploadedFile));
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponseDTO.error("Failed to upload file"));
}
}
@GetMapping("/download/{id}")
public ResponseEntity<Resource> downloadFile(@PathVariable Long id) {
try {
FileResource fileResource = fileService.getFile(id);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(fileResource.getContentType()))
.contentLength(fileResource.getSize())
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + fileResource.getFilename() + "\"")
.body(fileResource.getResource());
} catch (FileNotFoundException e) {
return ResponseEntity.notFound().build();
}
}
}
Authentication API
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@PostMapping("/login")
public ResponseEntity<ApiResponseDTO<LoginResponseDTO>> login(
@Valid @RequestBody LoginRequestDTO request) {
try {
LoginResponseDTO response = authService.authenticate(request);
return ResponseEntity.ok()
.header("Authorization", "Bearer " + response.getAccessToken())
.header("X-Token-Expires-In", String.valueOf(response.getExpiresIn()))
.body(ApiResponseDTO.success("Login successful", response));
} catch (BadCredentialsException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(ApiResponseDTO.error("Invalid credentials"));
}
}
@PostMapping("/refresh")
public ResponseEntity<ApiResponseDTO<TokenResponseDTO>> refreshToken(
@RequestHeader("Authorization") String refreshToken) {
try {
TokenResponseDTO response = authService.refreshToken(refreshToken);
return ResponseEntity.ok()
.header("Authorization", "Bearer " + response.getAccessToken())
.body(ApiResponseDTO.success("Token refreshed successfully", response));
} catch (TokenExpiredException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(ApiResponseDTO.error("Refresh token expired"));
}
}
@PostMapping("/logout")
public ResponseEntity<ApiResponseDTO<Void>> logout(
@RequestHeader("Authorization") String token) {
authService.logout(token);
return ResponseEntity.ok()
.header("X-Logout-Time", String.valueOf(System.currentTimeMillis()))
.body(ApiResponseDTO.success("Logout successful", null));
}
}
Conclusion
ResponseEntity
is a powerful tool in Spring Boot that provides complete control over HTTP responses. When combined with DTOs, it enables the creation of robust, type-safe REST APIs with proper status codes, headers, and response structures.
Key Takeaways:
- Always use appropriate HTTP status codes
- Implement consistent response structures with DTOs
- Handle errors gracefully with proper status codes
- Use headers effectively for metadata and caching
- Follow RESTful principles in your API design
- Implement proper validation and error handling