Spring MVC REST Controllers
Introduction
REST (Representational State Transfer) has become the standard architectural style for designing networked applications. Spring MVC provides excellent support for building RESTful web services through its annotation-based approach. In this tutorial, we'll explore how to create REST controllers in Spring MVC to build robust and scalable APIs.
REST controllers in Spring MVC allow you to create web services that adhere to REST principles, handling different HTTP methods (GET, POST, PUT, DELETE) and producing/consuming data in various formats (JSON, XML, etc.).
What is a REST Controller?
A REST controller in Spring MVC is a specialized component that handles HTTP requests and produces HTTP responses, typically in a data format like JSON or XML rather than HTML views. Unlike traditional MVC controllers that return view names, REST controllers directly return domain objects which are automatically converted to the requested response format.
The key annotation for creating REST controllers in Spring MVC is @RestController
, which is a convenience annotation that combines @Controller
and @ResponseBody
.
Creating Your First REST Controller
Let's start by creating a simple REST controller that returns a greeting message:
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
@GetMapping("/greeting")
public String greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
return "Hello, " + name + "!";
}
}
How it works:
@RestController
marks the class as a controller where every method returns a domain object instead of a view@GetMapping("/greeting")
maps HTTP GET requests to the/greeting
endpoint to thegreeting()
method@RequestParam
binds thename
parameter from the query string to thename
method parameter
Example request and response:
Request:
GET /greeting?name=John
Response:
Hello, John!
Request:
GET /greeting
Response:
Hello, World!
Returning JSON Objects
In real-world applications, you typically return objects that are automatically converted to JSON:
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
@GetMapping("/greeting-json")
public Greeting greetingJson(@RequestParam(value = "name", defaultValue = "World") String name) {
return new Greeting(1, "Hello, " + name + "!");
}
}
class Greeting {
private final long id;
private final String content;
public Greeting(long id, String content) {
this.id = id;
this.content = content;
}
public long getId() {
return id;
}
public String getContent() {
return content;
}
}
Example request and response:
Request:
GET /greeting-json?name=John
Response:
{
"id": 1,
"content": "Hello, John!"
}
HTTP Methods in REST Controllers
Spring MVC provides annotations for mapping different HTTP methods to controller methods:
package com.example.demo;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final List<User> users = new ArrayList<>();
private final AtomicLong counter = new AtomicLong();
// GET all users
@GetMapping
public List<User> getAllUsers() {
return users;
}
// GET a specific user by ID
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return users.stream()
.filter(user -> user.getId().equals(id))
.findFirst()
.orElseThrow(() -> new UserNotFoundException(id));
}
// POST - create a new user
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public User createUser(@RequestBody User user) {
user.setId(counter.incrementAndGet());
users.add(user);
return user;
}
// PUT - update an existing user
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User updatedUser) {
return users.stream()
.filter(user -> user.getId().equals(id))
.findFirst()
.map(user -> {
user.setName(updatedUser.getName());
user.setEmail(updatedUser.getEmail());
return user;
})
.orElseThrow(() -> new UserNotFoundException(id));
}
// DELETE - remove a user
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteUser(@PathVariable Long id) {
boolean removed = users.removeIf(user -> user.getId().equals(id));
if (!removed) {
throw new UserNotFoundException(id);
}
}
}
class User {
private Long id;
private String name;
private String email;
// 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; }
}
@ResponseStatus(HttpStatus.NOT_FOUND)
class UserNotFoundException extends RuntimeException {
public UserNotFoundException(Long id) {
super("User not found with id: " + id);
}
}
Key points:
@RestController
indicates that all methods in this class will return JSON/XML responses@RequestMapping("/api/users")
sets the base path for all endpoints in this controller@GetMapping
,@PostMapping
,@PutMapping
,@DeleteMapping
map HTTP methods to handler methods@PathVariable
extracts values from the URI path@RequestBody
converts the incoming request body into an object@ResponseStatus
changes the default HTTP status code returned
Request and Response Headers
You can work with request and response headers in Spring MVC REST controllers:
@RestController
public class HeaderController {
@GetMapping("/check-header")
public String checkHeader(@RequestHeader("User-Agent") String userAgent) {
return "Your User-Agent is: " + userAgent;
}
@GetMapping("/custom-header")
public ResponseEntity<String> customHeader() {
return ResponseEntity.ok()
.header("Custom-Header", "Custom-Value")
.body("Check the response headers!");
}
}
Content Negotiation
Spring MVC supports content negotiation, allowing clients to request data in different formats:
@RestController
public class ContentNegotiationController {
@GetMapping(value = "/data",
produces = { "application/json", "application/xml" })
public Product getData() {
return new Product(1L, "Laptop", 999.99);
}
}
Clients can specify the desired format using the Accept header or file extensions (.json, .xml).
Handling Exceptions
Proper error handling is crucial in REST APIs. Spring MVC provides several ways to handle exceptions:
@RestController
@RequestMapping("/api/products")
public class ProductController {
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
// Simulate product lookup
if (id <= 0) {
throw new ProductNotFoundException(id);
}
return new Product(id, "Product " + id, 19.99);
}
// Exception handler method for this controller
@ExceptionHandler(ProductNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Map<String, String> handleProductNotFoundException(ProductNotFoundException ex) {
Map<String, String> errors = new HashMap<>();
errors.put("error", ex.getMessage());
errors.put("timestamp", LocalDateTime.now().toString());
return errors;
}
}
class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(Long id) {
super("Product not found with id: " + id);
}
}
For global exception handling across multiple controllers, you can use @ControllerAdvice
:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Map<String, String> handleProductNotFoundException(ProductNotFoundException ex) {
Map<String, String> errors = new HashMap<>();
errors.put("error", ex.getMessage());
errors.put("timestamp", LocalDateTime.now().toString());
return errors;
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Map<String, String> handleGeneralException(Exception ex) {
Map<String, String> errors = new HashMap<>();
errors.put("error", "An unexpected error occurred");
errors.put("message", ex.getMessage());
return errors;
}
}
Validation in REST Controllers
You can use Bean Validation API with Spring MVC to validate input data:
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Order createOrder(@Valid @RequestBody Order order) {
// Process the order
return order;
}
}
class Order {
private Long id;
@NotBlank(message = "Customer name is required")
private String customerName;
@NotEmpty(message = "Order must have at least one item")
private List<OrderItem> items;
@Min(value = 0, message = "Total amount must be positive")
private double totalAmount;
// Getters and setters
}
@RestControllerAdvice
public class ValidationExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, Object> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, Object> response = new HashMap<>();
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.toList());
response.put("errors", errors);
response.put("timestamp", LocalDateTime.now().toString());
return response;
}
}
Real-world Example: Building a RESTful API for a Task Manager
Let's put everything together in a real-world example of a task management API:
package com.example.taskmanager;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
@SpringBootApplication
public class TaskManagerApplication {
public static void main(String[] args) {
SpringApplication.run(TaskManagerApplication.class, args);
}
}
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
private final List<Task> tasks = new ArrayList<>();
private final AtomicLong idCounter = new AtomicLong();
// GET all tasks with optional filtering
@GetMapping
public List<Task> getTasks(@RequestParam(required = false) String status) {
if (status != null) {
return tasks.stream()
.filter(task -> status.equalsIgnoreCase(task.getStatus()))
.toList();
}
return tasks;
}
// GET a single task by ID
@GetMapping("/{id}")
public ResponseEntity<Task> getTaskById(@PathVariable Long id) {
Optional<Task> task = tasks.stream()
.filter(t -> t.getId().equals(id))
.findFirst();
return task.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}
// POST - create a new task
@PostMapping
public ResponseEntity<Task> createTask(@Valid @RequestBody Task task) {
task.setId(idCounter.incrementAndGet());
task.setCreatedDate(LocalDateTime.now());
task.setStatus("PENDING");
tasks.add(task);
return ResponseEntity.status(HttpStatus.CREATED).body(task);
}
// PUT - update task
@PutMapping("/{id}")
public ResponseEntity<Task> updateTask(@PathVariable Long id, @Valid @RequestBody Task updatedTask) {
Optional<Task> existingTask = tasks.stream()
.filter(t -> t.getId().equals(id))
.findFirst();
if (existingTask.isPresent()) {
Task task = existingTask.get();
task.setTitle(updatedTask.getTitle());
task.setDescription(updatedTask.getDescription());
task.setStatus(updatedTask.getStatus());
task.setUpdatedDate(LocalDateTime.now());
return ResponseEntity.ok(task);
} else {
return ResponseEntity.notFound().build();
}
}
// PATCH - update task status
@PatchMapping("/{id}/status")
public ResponseEntity<Task> updateTaskStatus(
@PathVariable Long id,
@RequestBody Map<String, String> statusUpdate) {
String newStatus = statusUpdate.get("status");
if (newStatus == null) {
return ResponseEntity.badRequest().build();
}
Optional<Task> existingTask = tasks.stream()
.filter(t -> t.getId().equals(id))
.findFirst();
if (existingTask.isPresent()) {
Task task = existingTask.get();
task.setStatus(newStatus.toUpperCase());
task.setUpdatedDate(LocalDateTime.now());
return ResponseEntity.ok(task);
} else {
return ResponseEntity.notFound().build();
}
}
// DELETE - remove a task
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteTask(@PathVariable Long id) {
boolean removed = tasks.removeIf(task -> task.getId().equals(id));
return removed ?
ResponseEntity.noContent().build() :
ResponseEntity.notFound().build();
}
}
class Task {
private Long id;
@NotBlank(message = "Title is required")
@Size(min = 3, max = 100, message = "Title must be between 3 and 100 characters")
private String title;
private String description;
private String status;
private LocalDateTime createdDate;
private LocalDateTime updatedDate;
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public LocalDateTime getCreatedDate() { return createdDate; }
public void setCreatedDate(LocalDateTime createdDate) { this.createdDate = createdDate; }
public LocalDateTime getUpdatedDate() { return updatedDate; }
public void setUpdatedDate(LocalDateTime updatedDate) { this.updatedDate = updatedDate; }
}
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleExceptions(Exception ex) {
Map<String, Object> body = new HashMap<>();
body.put("timestamp", LocalDateTime.now().toString());
body.put("message", ex.getMessage());
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
if (ex instanceof org.springframework.validation.BindException) {
status = HttpStatus.BAD_REQUEST;
body.put("errors", ((org.springframework.validation.BindException) ex).getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.toList());
}
return new ResponseEntity<>(body, status);
}
}
This example demonstrates:
- Creating a full CRUD API for tasks
- Using proper HTTP methods and status codes
- Input validation with error handling
- Using different endpoint types (collection endpoints, item endpoints, specific action endpoints)
- Filtering with query parameters
- Global exception handling
Testing REST Controllers
You can test Spring MVC REST controllers with MockMvc:
@WebMvcTest(TaskController.class)
public class TaskControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
public void testCreateTask() throws Exception {
Task task = new Task();
task.setTitle("Complete project");
task.setDescription("Finish the Spring MVC project");
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(task)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").exists())
.andExpect(jsonPath("$.title").value("Complete project"))
.andExpect(jsonPath("$.status").value("PENDING"));
}
// More test methods...
}
Summary
In this tutorial, we've covered:
- Creating REST Controllers: Using
@RestController
to create controllers that handle HTTP requests and return data instead of views - HTTP Methods: Mapping different HTTP methods (GET, POST, PUT, PATCH, DELETE) to controller methods
- Path Variables and Request Parameters: Extracting data from URLs and query strings
- Request and Response Handling: Working with request bodies, headers, and customizing responses
- Content Negotiation: Supporting different data formats (JSON, XML)
- Exception Handling: Using
@ExceptionHandler
and@RestControllerAdvice
for centralized error handling - Validation: Implementing input validation with Bean Validation API
- Real-world Example: Building a complete RESTful API for a task manager
- Testing: Writing tests for REST controllers with MockMvc
REST controllers are a powerful feature of Spring MVC that enable you to build robust and maintainable web APIs following REST principles.
Additional Resources
- Spring Official Documentation on REST Controllers
- Spring REST API Best Practices
- Richardson Maturity Model for RESTful APIs
Exercises
- Extend the Task Manager API to include user authentication and task ownership.
- Implement pagination and sorting for the task list endpoint.
- Add a feature to categorize tasks and filter by category.
- Implement file uploads for task attachments using
MultipartFile
. - Create a search endpoint that allows searching tasks by title or description.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)