Spring REST Controllers
Introduction
REST (Representational State Transfer) controllers are a fundamental component of Spring-based web applications that allow you to build RESTful web services. In Spring, REST controllers handle HTTP requests and generate appropriate responses following REST architectural principles. They serve as the entry point for client applications wanting to interact with your backend services.
Spring REST controllers utilize annotations to map HTTP requests to specific handler methods, simplifying the development of RESTful APIs. This allows developers to focus on business logic rather than the complexities of HTTP request processing.
In this tutorial, we'll explore how to create and implement REST controllers in Spring Boot, understand key annotations, and learn best practices for building robust RESTful services.
What Are Spring REST Controllers?
A REST controller in Spring is a component that handles web requests. Unlike traditional MVC controllers that often return view names, REST controllers typically return data that gets serialized directly to the HTTP response body (usually in JSON or XML format).
Spring provides the @RestController
annotation, which is a specialized version of the @Controller
annotation. It's specifically designed for RESTful web services and combines @Controller
with @ResponseBody
, indicating that the return value of methods should be bound to the web response body.
Setting Up Your Spring REST Project
Before diving into REST controllers, let's ensure we have a Spring Boot project set up with the necessary dependencies:
Required Dependencies
For a Spring Boot application with REST capabilities, you'll need the following dependencies in your pom.xml
(if using Maven):
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- For testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Or in your build.gradle
(if using Gradle):
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Creating Your First REST Controller
Let's create a simple REST controller to understand how it works:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String sayHello() {
return "Hello, World!";
}
}
This simple controller:
- Is annotated with
@RestController
to indicate it's a REST controller - Has a method
sayHello()
mapped to handle GET requests to the "/hello" path - Returns a String that will be sent directly to the client as the response body
When you start your Spring Boot application and navigate to http://localhost:8080/hello
in your browser, you'll see:
Hello, World!
Key Annotations for REST Controllers
Spring provides several annotations that make building REST APIs easier:
@RestController
Marks a class as a REST controller, combining @Controller
and @ResponseBody
.
Request Mapping Annotations
@RequestMapping
: General annotation for mapping requests@GetMapping
: Shortcut for@RequestMapping(method = RequestMethod.GET)
@PostMapping
: For HTTP POST requests@PutMapping
: For HTTP PUT requests@DeleteMapping
: For HTTP DELETE requests@PatchMapping
: For HTTP PATCH requests
Request Parameter Annotations
@PathVariable
: Extracts values from URI path@RequestParam
: Extracts query parameters@RequestBody
: Maps the request body to an object@RequestHeader
: Maps header information
Let's see these in action:
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping("/api/products")
public class ProductController {
// In-memory product storage (would use a database in real applications)
private List<Product> products = new ArrayList<>();
private long nextId = 1;
// GET all products
@GetMapping
public List<Product> getAllProducts() {
return products;
}
// GET a single product by ID
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable long id) {
Optional<Product> product = products.stream()
.filter(p -> p.getId() == id)
.findFirst();
return product
.map(p -> ResponseEntity.ok(p))
.orElse(ResponseEntity.notFound().build());
}
// POST create a new product
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Product createProduct(@RequestBody Product product) {
product.setId(nextId++);
products.add(product);
return product;
}
// PUT update an existing product
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(
@PathVariable long id,
@RequestBody Product updatedProduct) {
for (int i = 0; i < products.size(); i++) {
if (products.get(i).getId() == id) {
updatedProduct.setId(id);
products.set(i, updatedProduct);
return ResponseEntity.ok(updatedProduct);
}
}
return ResponseEntity.notFound().build();
}
// DELETE a product
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable long id) {
boolean removed = products.removeIf(p -> p.getId() == id);
if (removed) {
return ResponseEntity.noContent().build();
} else {
return ResponseEntity.notFound().build();
}
}
// Search products by name (using query parameter)
@GetMapping("/search")
public List<Product> searchProducts(@RequestParam String name) {
return products.stream()
.filter(p -> p.getName().toLowerCase().contains(name.toLowerCase()))
.toList();
}
}
// Product model class
class Product {
private long id;
private String name;
private String description;
private double price;
// 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 getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
}
Response Handling in REST Controllers
Spring provides flexible ways to handle different types of responses:
Using ResponseEntity
The ResponseEntity
class gives you full control over the HTTP response including status code, headers, and body:
@GetMapping("/custom-response")
public ResponseEntity<String> customResponse() {
HttpHeaders headers = new HttpHeaders();
headers.add("Custom-Header", "value");
return new ResponseEntity<>(
"Response with custom header and status code",
headers,
HttpStatus.OK
);
}
Response Status Codes
You can specify the response status using @ResponseStatus
or within ResponseEntity
:
@GetMapping("/not-found")
@ResponseStatus(HttpStatus.NOT_FOUND)
public String notFound() {
return "This will return a 404 status code";
}
@GetMapping("/created")
public ResponseEntity<String> created() {
return ResponseEntity.status(HttpStatus.CREATED)
.body("This will return a 201 status code");
}
Handling Request Parameters
Spring REST controllers offer different ways to extract data from requests:
Path Variables
Path variables are parts of the URL path that are extracted as parameters:
@GetMapping("/users/{userId}/books/{bookId}")
public String getBook(@PathVariable Long userId, @PathVariable Long bookId) {
return "User: " + userId + ", Book: " + bookId;
}
Query Parameters
Query parameters come after the ?
in a URL:
@GetMapping("/books")
public String getBooks(
@RequestParam(required = false, defaultValue = "1") int page,
@RequestParam(required = false, defaultValue = "10") int size,
@RequestParam(required = false) String genre
) {
return "Getting books - Page: " + page + ", Size: " + size +
(genre != null ? ", Genre: " + genre : "");
}
Request Body
For POST, PUT, and PATCH requests, data is often sent in the request body:
@PostMapping("/books")
public Book createBook(@RequestBody Book book) {
// Persist the book and return it with an ID
book.setId(generateId());
return book;
}
Content Negotiation
Spring supports content negotiation, allowing clients to specify the desired format of the response:
@RestController
public class ContentNegotiationController {
@GetMapping(
value = "/data",
produces = {
"application/json",
"application/xml"
}
)
public DataObject getData() {
return new DataObject("sample data");
}
}
With appropriate converters configured, Spring will automatically format the response based on the client's Accept
header.
Exception Handling in REST Controllers
Proper error handling is crucial for robust REST APIs. Spring provides:
@ExceptionHandler
Handles exceptions thrown from controller methods:
@RestController
public class ProductController {
// Controller methods...
@ExceptionHandler(ProductNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleProductNotFound(ProductNotFoundException ex) {
return new ErrorResponse("PRODUCT_NOT_FOUND", ex.getMessage());
}
}
@ControllerAdvice / @RestControllerAdvice
Provides centralized exception handling across all controllers:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleProductNotFound(ProductNotFoundException ex) {
return new ErrorResponse("PRODUCT_NOT_FOUND", ex.getMessage());
}
@ExceptionHandler(InvalidInputException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleInvalidInput(InvalidInputException ex) {
return new ErrorResponse("INVALID_INPUT", ex.getMessage());
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleGenericException(Exception ex) {
return new ErrorResponse("SERVER_ERROR", "An unexpected error occurred");
}
}
class ErrorResponse {
private String code;
private String message;
public ErrorResponse(String code, String message) {
this.code = code;
this.message = message;
}
// Getters
public String getCode() { return code; }
public String getMessage() { return message; }
}
Real-World Example: Building a Todo API
Let's bring all concepts together by building a simple Todo API:
@RestController
@RequestMapping("/api/todos")
public class TodoController {
private final TodoService todoService;
// Using constructor injection for better testability
public TodoController(TodoService todoService) {
this.todoService = todoService;
}
@GetMapping
public List<Todo> getAllTodos(
@RequestParam(required = false) Boolean completed) {
if (completed != null) {
return todoService.findByCompleted(completed);
}
return todoService.findAll();
}
@GetMapping("/{id}")
public ResponseEntity<Todo> getTodoById(@PathVariable Long id) {
return todoService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<Todo> createTodo(@RequestBody @Valid TodoRequest todoRequest) {
Todo created = todoService.create(todoRequest);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity.created(location).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<Todo> updateTodo(
@PathVariable Long id,
@RequestBody @Valid TodoRequest todoRequest) {
return todoService.update(id, todoRequest)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteTodo(@PathVariable Long id) {
if (todoService.deleteById(id)) {
return ResponseEntity.noContent().build();
}
return ResponseEntity.notFound().build();
}
// Mark todo as completed or not completed
@PatchMapping("/{id}/complete")
public ResponseEntity<Todo> setCompleted(
@PathVariable Long id,
@RequestParam boolean completed) {
return todoService.setCompleted(id, completed)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
}
// Simple DTO for Todo creation and updates
class TodoRequest {
@NotBlank(message = "Title is required")
@Size(max = 100, message = "Title cannot exceed 100 characters")
private String title;
private String description;
private boolean completed = false;
// Getters and setters
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 boolean isCompleted() { return completed; }
public void setCompleted(boolean completed) { this.completed = completed; }
}
// Todo entity class
class Todo {
private Long id;
private String title;
private String description;
private boolean completed;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// Constructors, getters, and setters would be here
}
Best Practices for REST Controllers
Here are some best practices to follow when designing REST controllers:
-
Use proper HTTP methods:
- GET for retrieving resources
- POST for creating resources
- PUT for replacing resources
- PATCH for partially updating resources
- DELETE for removing resources
-
Return appropriate HTTP status codes:
- 200 OK: The request was successful
- 201 Created: A resource was successfully created
- 204 No Content: The request was successful but there's no content to return
- 400 Bad Request: Client-side error
- 404 Not Found: Resource not found
- 500 Internal Server Error: Server-side error
-
Follow REST naming conventions:
- Use nouns rather than verbs (e.g.,
/products
not/getProducts
) - Use plural nouns for collections
- Use consistent casing (kebab-case is common for URLs)
- Use nouns rather than verbs (e.g.,
-
Implement validation: Validate request data to ensure it meets requirements before processing.
-
Version your API: Include version information in URLs or headers (e.g.,
/api/v1/products
). -
Implement proper error handling: Return clear error messages that help clients understand what went wrong.
-
Use DTOs (Data Transfer Objects): Separate your API models from your domain models.
-
Implement pagination: For endpoints that might return large data sets.
-
Use hypermedia links: Consider implementing HATEOAS (Hypertext as the Engine of Application State) for better API navigation.
-
Document your API: Use tools like Swagger/OpenAPI to document your endpoints.
Summary
Spring REST controllers are powerful components for building RESTful APIs. In this tutorial, we've covered:
- How to create basic REST controllers
- Request mapping and handling
- Processing path variables, query parameters, and request bodies
- Response handling and status codes
- Exception handling
- Content negotiation
- Best practices for RESTful API design
With these tools, you can build robust, maintainable, and scalable RESTful services using Spring Boot.
Additional Resources and Exercises
Resources for Further Learning
- Spring Framework Documentation
- Spring Boot REST Tutorial
- RESTful API Design Best Practices
- Richardson Maturity Model
Exercises
-
Basic REST API: Build a simple REST API for a library that can manage books and authors.
-
CRUD Operations: Implement full CRUD operations for a resource of your choice (e.g., products, users, etc.).
-
Validation and Error Handling: Add validation to your API and implement proper error handling with custom error responses.
-
Pagination and Sorting: Add pagination and sorting capabilities to a collection endpoint.
-
Filtering: Implement filtering by different fields on a collection endpoint.
-
Security: Add basic authentication to your API endpoints using Spring Security.
-
Testing: Write unit and integration tests for your controllers using tools like JUnit and MockMvc.
By working through these exercises, you'll gain practical experience building REST controllers in Spring and be well on your way to creating production-ready APIs.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)