Microservices · Lesson 4
Building REST Controllers
Understanding REST Controllers
A REST controller in Spring Boot handles HTTP requests and returns responses, typically in JSON format. REST (Representational State Transfer) uses HTTP methods (GET, POST, PUT, DELETE) to perform operations on resources. A Spring Boot @RestController combines @Controller and @ResponseBody, automatically converting return values to JSON. This makes building REST APIs incredibly simple.
Defining REST Endpoints with Annotations
Use @GetMapping, @PostMapping, @PutMapping, @DeleteMapping to map HTTP methods to Java methods. Each method handles a specific endpoint and HTTP method combination. Parameters come from the URL path (@PathVariable), query string (@RequestParam), or request body (@RequestBody). Spring Boot automatically parses and validates them.
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users") // Base URL path
public class UserController {
private List<User> users = new ArrayList<>();
// GET /api/users
@GetMapping
public List<User> getAllUsers() {
return users;
}
// GET /api/users/123
@GetMapping("/{id}")
public User getUserById(@PathVariable int id) {
return users.stream()
.filter(u -> u.getId() == id)
.findFirst()
.orElse(null);
}
// GET /api/users?name=Alice
@GetMapping("/search")
public List<User> searchByName(@RequestParam String name) {
return users.stream()
.filter(u -> u.getName().contains(name))
.collect(java.util.stream.Collectors.toList());
}
// POST /api/users with JSON body
@PostMapping
public User createUser(@RequestBody User user) {
user.setId(users.size() + 1);
users.add(user);
return user;
}
// PUT /api/users/123 with JSON body
@PutMapping("/{id}")
public User updateUser(@PathVariable int id, @RequestBody User updatedUser) {
User existing = getUserById(id);
if (existing != null) {
existing.setName(updatedUser.getName());
existing.setEmail(updatedUser.getEmail());
}
return existing;
}
// DELETE /api/users/123
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable int id) {
users.removeIf(u -> u.getId() == id);
}
}
class User {
private int id;
private String name;
private String email;
// Constructors, getters, setters...
User() {}
User(int id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public int getId() { return id; }
public void setId(int 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; }
}
HTTP Status Codes and Response Entities
REST APIs should return appropriate HTTP status codes: 200 (OK), 201 (Created), 204 (No Content), 400 (Bad Request), 401 (Unauthorized), 404 (Not Found), 500 (Server Error). Use ResponseEntity to control status codes, headers, and body. This gives clients precise information about request success/failure.
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@RestController
@RequestMapping("/api/users")
public class UserController {
private Map<Integer, User> users = new HashMap<>();
// Returns 200 OK with user data
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable int id) {
User user = users.get(id);
if (user == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); // 404
}
return ResponseEntity.ok(user); // 200 OK
}
// Returns 201 Created with Location header
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
int newId = users.size() + 1;
user.setId(newId);
users.put(newId, user);
return ResponseEntity
.status(HttpStatus.CREATED) // 201
.header("Location", "/api/users/" + newId)
.body(user);
}
// Returns 204 No Content (successful deletion, no body)
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable int id) {
if (!users.containsKey(id)) {
return ResponseEntity.notFound().build(); // 404
}
users.remove(id);
return ResponseEntity.noContent().build(); // 204
}
// Returns 400 Bad Request for invalid input
@PostMapping("/validate")
public ResponseEntity<String> validateUser(@RequestBody User user) {
if (user.getName() == null || user.getName().isEmpty()) {
return ResponseEntity
.badRequest() // 400
.body("Name is required");
}
return ResponseEntity.ok("Valid");
}
}
Content Negotiation: JSON and XML
Spring Boot automatically serializes response objects to JSON. You can configure it to support XML, HAL JSON, or other formats using @JsonView, produces, and consumes attributes. Content negotiation allows clients to request their preferred format.
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.web.bind.annotation.*;
// User class with JSON customization
class User {
@JsonProperty("user_id") // Rename in JSON
private int id;
private String name;
// Getters, setters...
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
@RestController
@RequestMapping("/api/users")
public class UserController {
// Produces JSON (default)
@GetMapping("/{id}")
public User getUser(@PathVariable int id) {
return new User(id, "Alice");
}
// Explicitly specify content type
@PostMapping(consumes = "application/json", produces = "application/json")
public User createUser(@RequestBody User user) {
return user;
}
}
Exception Handling with @ExceptionHandler
Handle exceptions globally or at the controller level. Use @ExceptionHandler to catch exceptions and return appropriate error responses. This keeps error handling logic centralized and responses consistent.
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
// Custom exception
class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
// Error response structure
class ErrorResponse {
private int status;
private String message;
private long timestamp;
ErrorResponse(int status, String message) {
this.status = status;
this.message = message;
this.timestamp = System.currentTimeMillis();
}
public int getStatus() { return status; }
public String getMessage() { return message; }
public long getTimestamp() { return timestamp; }
}
@RestController
@RequestMapping("/api/users")
public class UserController {
private Map<Integer, User> users = new HashMap<>();
@GetMapping("/{id}")
public User getUser(@PathVariable int id) {
return users.get(id);
// If null, throw exception
// throw new ResourceNotFoundException("User " + id + " not found");
}
// Handle ResourceNotFoundException
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(404, ex.getMessage());
return ResponseEntity.status(404).body(error);
}
// Handle generic exceptions
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
ErrorResponse error = new ErrorResponse(500, "Internal Server Error: " + ex.getMessage());
return ResponseEntity.status(500).body(error);
}
}
| HTTP Method | Purpose | Expected Status | Example |
|---|---|---|---|
| GET | Retrieve resource | 200 OK or 404 Not Found | GET /api/users/123 |
| POST | Create resource | 201 Created or 400 Bad Request | POST /api/users with JSON |
| PUT | Update entire resource | 200 OK or 404 Not Found | PUT /api/users/123 with JSON |
| PATCH | Partial update | 200 OK or 404 Not Found | PATCH /api/users/123 |
| DELETE | Delete resource | 204 No Content or 404 | DELETE /api/users/123 |
Common Mistake: Returning status 200 for all scenarios, including errors. Clients can't distinguish success from failure by response body alone. Always return appropriate status codes: 2xx for success, 4xx for client errors, 5xx for server errors. Also, avoid exposing internal exceptions in error messages — users see "Internal Server Error" while logs have full details.
Best Practices for REST Controllers
- Use appropriate HTTP methods: GET for retrieval, POST for creation, PUT/PATCH for updates, DELETE for deletion
- Return appropriate HTTP status codes: 201 for creation, 404 for not found, 400 for bad requests
- Use ResponseEntity for fine-grained control over status, headers, and body
- Validate input in the controller; return 400 Bad Request if invalid
- Implement exception handling globally with
@ControllerAdviceand@ExceptionHandler - Version your API (e.g.,
/api/v1/users) to support gradual updates - Use consistent naming: plural nouns for collections (
/users), singular for resources (/users/{id}) - Document endpoints with Swagger/SpringFox (
@Api,@ApiOperation)
Real-World Example: Product REST API
A Product API controller handles CRUD operations: GET /api/v1/products returns all products; GET /api/v1/products/ returns a specific product (404 if not found); POST /api/v1/products creates a new product (201 with Location header); PUT /api/v1/products/ updates a product (200 or 404); DELETE /api/v1/products/ deletes a product (204 or 404). Input validation returns 400 if product name is missing. Database errors return 500 with a generic message while logging the actual error. ResponseEntity is used throughout to control status codes and headers. Exception handling catches all exceptions and returns error responses with timestamps and messages.
Quiz Check