Skip to main content

Spring REST Request Handling

Introduction

When building RESTful web services with Spring, understanding how to handle HTTP requests effectively is crucial. Spring provides a rich set of annotations and mechanisms that make request handling straightforward and flexible.

In this tutorial, we'll explore how Spring REST handles different types of requests, extracts data from various parts of HTTP requests, and maps this data to Java objects. Whether you're building a simple API or a complex microservice, mastering these concepts will help you create robust and maintainable REST services.

Understanding Spring REST Controllers

At the core of Spring REST request handling is the concept of controllers. Controllers are responsible for processing incoming HTTP requests, executing business logic, and returning responses.

Basic Controller Structure

java
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.GetMapping;

@RestController
@RequestMapping("/api")
public class MyRestController {

@GetMapping("/hello")
public String sayHello() {
return "Hello, World!";
}
}

In this example:

  • @RestController indicates that this class serves as a REST controller
  • @RequestMapping("/api") sets the base path for all endpoints in this controller
  • @GetMapping("/hello") maps HTTP GET requests to /api/hello to the sayHello() method

Handling Different HTTP Methods

Spring REST provides annotations for all standard HTTP methods:

java
@RestController
@RequestMapping("/api/books")
public class BookController {

// GET request to fetch all books
@GetMapping
public List<Book> getAllBooks() {
// code to retrieve all books
return bookService.findAll();
}

// GET request to fetch a specific book
@GetMapping("/{id}")
public Book getBookById(@PathVariable Long id) {
// code to retrieve a book by ID
return bookService.findById(id);
}

// POST request to create a new book
@PostMapping
public Book createBook(@RequestBody Book book) {
// code to save a new book
return bookService.save(book);
}

// PUT request to update a book
@PutMapping("/{id}")
public Book updateBook(@PathVariable Long id, @RequestBody Book book) {
// code to update an existing book
return bookService.update(id, book);
}

// DELETE request to remove a book
@DeleteMapping("/{id}")
public void deleteBook(@PathVariable Long id) {
// code to delete a book
bookService.delete(id);
}
}

Extracting Data from Requests

Spring provides multiple ways to extract data from incoming HTTP requests:

1. Path Variables

Path variables are parts of the URL that are dynamic and can be extracted as method parameters.

java
@GetMapping("/users/{userId}/posts/{postId}")
public Post getUserPost(
@PathVariable Long userId,
@PathVariable Long postId
) {
return postService.findUserPost(userId, postId);
}

You can also customize the path variable name:

java
@GetMapping("/users/{id}")
public User getUser(@PathVariable("id") Long userId) {
return userService.findById(userId);
}

2. Request Parameters

Request parameters are added to the URL after a question mark (?) and can be easily extracted:

java
// Handles: GET /api/products?category=electronics&sort=price
@GetMapping("/products")
public List<Product> getProductsByCategoryAndSort(
@RequestParam String category,
@RequestParam(defaultValue = "name") String sort
) {
return productService.findByCategory(category, sort);
}

Optional request parameters:

java
@GetMapping("/search")
public List<Product> searchProducts(
@RequestParam(required = false) String name,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size
) {
return productService.search(name, page, size);
}

3. Request Body

For POST, PUT, and PATCH requests, data is often sent in the request body. Spring can automatically deserialize this data into Java objects:

java
@PostMapping("/users")
public User createUser(@RequestBody User user) {
return userService.save(user);
}

Example request:

POST /api/users
Content-Type: application/json

{
"name": "John Doe",
"email": "[email protected]",
"age": 30
}

4. Request Headers

You can extract HTTP headers using the @RequestHeader annotation:

java
@GetMapping("/secured-resource")
public Resource getSecuredResource(
@RequestHeader("Authorization") String authHeader
) {
// Validate authentication token from header
securityService.validateToken(authHeader);
return resourceService.getSecuredResource();
}

For optional headers:

java
@GetMapping("/resource")
public Resource getResource(
@RequestHeader(value = "User-Agent", required = false) String userAgent
) {
log.info("Request from User-Agent: {}", userAgent);
return resourceService.getResource();
}

Handling Request Parameters with Complex Objects

Spring can also bind query parameters to object properties:

java
public class ProductFilter {
private String category;
private Double minPrice;
private Double maxPrice;
private String brand;

// getters and setters
}

@GetMapping("/products/filter")
public List<Product> filterProducts(ProductFilter filter) {
return productService.filter(
filter.getCategory(),
filter.getMinPrice(),
filter.getMaxPrice(),
filter.getBrand()
);
}

With this approach, a request like /products/filter?category=electronics&minPrice=100&maxPrice=500&brand=Samsung will automatically populate the ProductFilter object.

Request Validation

Spring integrates with Bean Validation (JSR-380) to validate incoming requests:

java
public class CreateUserRequest {
@NotBlank(message = "Name is required")
private String name;

@Email(message = "Email should be valid")
@NotBlank(message = "Email is required")
private String email;

@Min(value = 18, message = "Age should be at least 18")
private int age;

// getters and setters
}

@PostMapping("/users")
public User createUser(@Valid @RequestBody CreateUserRequest request) {
User user = new User();
user.setName(request.getName());
user.setEmail(request.getEmail());
user.setAge(request.getAge());
return userService.save(user);
}

To handle validation errors:

java
@RestControllerAdvice
public class ValidationExceptionHandler {

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();

ex.getBindingResult().getAllErrors().forEach(error -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});

return errors;
}
}

Handling File Uploads

Spring makes it easy to handle file uploads in REST APIs:

java
@PostMapping("/upload")
public String uploadFile(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return "Please select a file to upload";
}

try {
// Get the file name
String fileName = StringUtils.cleanPath(file.getOriginalFilename());

// Save the file on the server
Path targetLocation = Paths.get("uploads").resolve(fileName);
Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);

return "File uploaded successfully: " + fileName;
} catch (IOException ex) {
return "Could not upload file: " + ex.getMessage();
}
}

For multiple files:

java
@PostMapping("/upload-multiple")
public List<String> uploadMultipleFiles(@RequestParam("files") MultipartFile[] files) {
List<String> uploadedFiles = new ArrayList<>();

for (MultipartFile file : files) {
if (!file.isEmpty()) {
try {
String fileName = StringUtils.cleanPath(file.getOriginalFilename());
Path targetLocation = Paths.get("uploads").resolve(fileName);
Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);
uploadedFiles.add(fileName);
} catch (IOException ex) {
// Handle exception
}
}
}

return uploadedFiles;
}

Real-World Example: Building a Book API

Let's bring everything together in a comprehensive example of a Book API:

java
@RestController
@RequestMapping("/api/books")
public class BookController {

private final BookService bookService;

// Constructor injection of BookService
public BookController(BookService bookService) {
this.bookService = bookService;
}

@GetMapping
public Page<Book> getAllBooks(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String genre,
@RequestParam(defaultValue = "title") String sortBy) {

return bookService.findBooks(page, size, genre, sortBy);
}

@GetMapping("/{id}")
public ResponseEntity<Book> getBookById(@PathVariable Long id) {
return bookService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Book createBook(@Valid @RequestBody Book book) {
return bookService.save(book);
}

@PutMapping("/{id}")
public ResponseEntity<Book> updateBook(
@PathVariable Long id,
@Valid @RequestBody Book book) {

if (!bookService.existsById(id)) {
return ResponseEntity.notFound().build();
}

book.setId(id);
return ResponseEntity.ok(bookService.save(book));
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteBook(@PathVariable Long id) {
if (!bookService.existsById(id)) {
return ResponseEntity.notFound().build();
}

bookService.deleteById(id);
return ResponseEntity.noContent().build();
}

@GetMapping("/search")
public List<Book> searchBooks(@RequestParam String keyword) {
return bookService.search(keyword);
}

@PostMapping("/{bookId}/reviews")
@ResponseStatus(HttpStatus.CREATED)
public Review addReview(
@PathVariable Long bookId,
@Valid @RequestBody Review review) {

return bookService.addReview(bookId, review);
}
}

This controller covers:

  • Pagination and sorting
  • Searching and filtering
  • Proper HTTP status codes
  • Validation
  • Nested resources (book reviews)
  • Error handling with ResponseEntity

Advanced Request Handling Techniques

1. Request Mapping with Consumes and Produces

You can specify which media types your controllers can handle and produce:

java
@PostMapping(
value = "/books",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
public Book createBook(@RequestBody Book book) {
return bookService.save(book);
}

@PostMapping(
value = "/books",
consumes = MediaType.APPLICATION_XML_VALUE,
produces = MediaType.APPLICATION_XML_VALUE
)
public Book createBookFromXml(@RequestBody Book book) {
return bookService.save(book);
}

2. Using ResponseEntity for Complete Control

ResponseEntity gives you full control over the HTTP response:

java
@GetMapping("/users/{id}")
public ResponseEntity<?> getUser(@PathVariable Long id) {
try {
User user = userService.findById(id);
if (user == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok()
.header("Last-Modified", new Date().toString())
.body(user);
} catch (Exception e) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Error retrieving user: " + e.getMessage());
}
}

3. Custom Request Processing with HandlerMethodArgumentResolver

For complex request handling needs, you can create custom argument resolvers:

java
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(User.class) &&
parameter.hasParameterAnnotation(CurrentUser.class);
}

@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {

String token = webRequest.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
return userService.getUserFromToken(token);
}
return null;
}
}

// Custom annotation
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {}

// Usage in controller
@GetMapping("/profile")
public User getProfile(@CurrentUser User user) {
return user;
}

Summary

Spring REST provides a powerful and flexible system for handling HTTP requests. We've covered:

  • Setting up REST controllers with @RestController and request mapping annotations
  • Working with different HTTP methods (GET, POST, PUT, DELETE)
  • Extracting data from path variables, query parameters, request bodies, and headers
  • Validating incoming request data
  • Handling file uploads
  • Advanced techniques like content negotiation and custom argument resolvers

By mastering these concepts, you'll be well-equipped to build robust and flexible REST APIs with Spring.

Additional Resources

Exercises

  1. Create a simple REST API for a task management application with endpoints to create, read, update, and delete tasks.

  2. Extend your API to support filtering tasks by status (complete/incomplete) and sorting by due date.

  3. Implement validation for the task creation endpoint that ensures task names are not empty and due dates are in the future.

  4. Add the ability to upload attachments to tasks and retrieve them later.

  5. Implement a search endpoint that can search tasks by name or description with pagination support.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)