A Comprehensive Guide to Implementing and Consuming REST Endpoints in Spring Boot
REST, or Representational State Transfer, is a way for applications to communicate over the internet using standard HTTP methods like GET, POST, PUT, and DELETE. It allows clients, such as web or mobile apps, and backend services to interact with server resources, which typically consist of data. Each request contains all the information needed for the server to respond, making it stateless and efficient.
This guide will first cover how to implement REST endpoints in Spring Boot and then explore how to consume them using three key tools: OpenFeign, WebClient, and RestTemplate. You’ll learn how to manage HTTP responses, send objects as response bodies, set response statuses and headers, handle exceptions at the endpoint level, and use @RequestBody
to receive data from clients.
Part 1: Implementing REST Endpoints
When building REST endpoints in Spring Boot, the goal is to send data directly from the server to the client. This is a continuation of the previous post about building web applications with Spring Boot and Spring MVC. Let’s look at the basics of setting up REST controllers in Spring.
Setting Up a REST Controller
In Spring, REST endpoints return data, often as JSON, instead of rendering a view like a traditional MVC controller. To set this up, use the @RestController
annotation. This annotation combines @Controller
and @ResponseBody
, ensuring all methods return data directly.
First, create a simple Country
class:
public class Country {
private int id;
private String name;
public Country(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
}
Next, create a CountryService
that provides methods to retrieve country data:
@Service
public class CountryService {
private List<Country> countries = new ArrayList<>();
public CountryService() {
// Adding some sample countries
countries.add(new Country(1, "United States"));
countries.add(new Country(2, "Canada"));
countries.add(new Country(3, "Mexico"));
}
public List<Country> getAllCountries() {
return countries;
}
public Country getCountryById(int id) {
return countries.stream()
.filter(country -> country.getId() == id)
.findFirst()
.orElse(null);
}
}
Now, you can set up the CountryController
:
@RestController
public class CountryController {
@Autowired
private CountryService countryService;
@GetMapping("/countries")
public List<Country> getCountries() {
return countryService.getAllCountries();
}
@GetMapping("/countries/{id}")
public Country getCountryById(@PathVariable int id) {
return countryService.getCountryById(id);
}
}
In this example, the CountryController
uses the CountryService
to return JSON-formatted data. When you hit the /countries
endpoint, you receive a list of countries, and for /countries/{id}
, you get the specific country details based on the ID provided.
Controlling HTTP Responses
By default, Spring returns “200 OK” responses along with the data, but you can customize this using ResponseEntity
. With ResponseEntity
, you can control the status code, headers, and body of the response.
This code checks if the requested country exists and returns the corresponding response with the appropriate status code.
@GetMapping("/countries/{id}")
public ResponseEntity<Country> getCountry(@PathVariable int id) {
Country country = countryService.getCountryById(id);
if (country != null) {
return ResponseEntity.ok(country); // Returns 200 OK
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(null); // Returns 404 Not Found
}
}
This gives you finer control over the HTTP response, such as returning a “202 Accepted” or “404 Not Found” status if needed.
Handling Requests with @RequestBody
When a client sends data to the server, you can use @RequestBody
to map the incoming JSON data to a Java object. This is useful for creating or updating resources. In this context, the Country
object acts as a Data Transfer Object (DTO) used to transfer data between the client and the server.
@PostMapping("/countries")
public ResponseEntity<Country> addCountry(@RequestBody Country country) {
countryService.addCountry(country);
return ResponseEntity.status(HttpStatus.CREATED).body(country);
}
In this example, Spring converts the incoming JSON into a Country object. If the JSON is malformed, Spring returns a “400 Bad Request” response.
Centralized Error Handling with @RestControllerAdvice
Instead of catching exceptions in every controller method, you can use @RestControllerAdvice
to centralize error handling. This allows you to manage exceptions in one place and return custom error responses. Below is an example that handles a custom exception for when a country is not found.
First, create a custom exception:
public class CountryNotFoundException extends RuntimeException {
public CountryNotFoundException(String message) {
super(message);
}
}
Next, modify the CountryController
to throw this exception if a country is not found:
@GetMapping("/countries/{id}")
public ResponseEntity<Country> getCountryById(@PathVariable int id) {
Country country = countryService.getCountryById(id);
if (country == null) {
throw new CountryNotFoundException("Country not found with id: " + id);
}
return ResponseEntity.ok(country); // Returns 200 OK
}
Now, create a centralized exception handler using @RestControllerAdvice
:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CountryNotFoundException.class)
public ResponseEntity<ErrorDetails> handleCountryNotFound(CountryNotFoundException ex) {
ErrorDetails errorDetails = new ErrorDetails("Country Not Found", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorDetails);
}
}
Here’s the ErrorDetails class, which contains the error message and other details:
public class ErrorDetails {
private String title; // Title of the error
private String message; // Detailed error message
// Omit getters and setters for simplicity
}
In this example, when a CountryNotFoundException
is thrown, the centralized error handler returns a “404 Not Found” status along with a custom error message.
Part 2: Consuming REST Endpoints
In many cases, a backend app also needs to consume REST endpoints exposed by another app. In Spring Boot, you can do this using three main tools: OpenFeign, WebClient, and RestTemplate. For these examples, we’ll use a free online fake REST API, JSONPlaceholder, which provides fake data for testing. However, you can also create your own local REST API if you prefer.
1. OpenFeign: Simplified REST Client
OpenFeign
simplifies the process of consuming REST APIs by automatically generating clients from defined interfaces. To get started, add the necessary dependency in your pom.xml
:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
Next, add the Spring Cloud BOM (Bill of Materials) to manage versions of Spring Cloud dependencies. The Spring team recommends using a BOM to ensure consistent version management and prevent conflicts among the many sub-dependencies in Spring Cloud. While the Spring Boot starter parent manages core Spring Boot dependencies, the BOM ensures that all Spring Cloud components work seamlessly together.
<properties>
<spring-cloud.version>2023.0.2</spring-cloud.version> <!-- Use the latest version -->
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
If you ever need to find the correct version for Spring Cloud, the official Spring Cloud documentation is the best resource.
Next, enable OpenFeign
in your main Spring Boot application class with the @EnableFeignClients
annotation. This annotation tells Spring to scan for interfaces annotated with @FeignClient
, allowing it to generate the necessary implementations.
@SpringBootApplication
@EnableFeignClients
public class MyApp {
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}
}
Now, define your client interface using the @FeignClient
annotation. This is how you specify the name and URL of the API you’ll be calling:
@FeignClient(name = "jsonPlaceholderClient", url = "https://jsonplaceholder.typicode.com")
public interface JsonPlaceholderClient {
@GetMapping("/todos")
List<Todo> getTodos();
@GetMapping("/todos/{id}")
Todo getTodoById(@PathVariable int id);
}
The name parameter gives your client a unique identifier, while the url parameter specifies where the API is located. The magic of OpenFeign
is that Spring Cloud automatically provides the implementation for this interface. All you need to do is wire it into your application.
To handle the business logic, create a TodoService
class that uses the Feign client:
@Service
public class TodoService {
private final JsonPlaceholderClient jsonPlaceholderClient;
@Autowired
public TodoService(JsonPlaceholderClient jsonPlaceholderClient) {
this.jsonPlaceholderClient = jsonPlaceholderClient;
}
public List<Todo> getAllTodos() {
return jsonPlaceholderClient.getTodos();
}
public Todo getTodoById(int id) {
return jsonPlaceholderClient.getTodoById(id);
}
}
In this service, the jsonPlaceholderClient
is injected, allowing you to call the API methods you defined in the interface. The getAllTodos()
and getTodoById()
methods use the client to fetch data.
To make sense of the data you expect from the API, create a Todo class that matches the structure of the JSON response. Use @JsonProperty
to map the JSON fields to your class properties:
public class Todo {
@JsonProperty("userId")
private int userId;
@JsonProperty("id")
private int id;
@JsonProperty("title")
private String title;
@JsonProperty("completed")
private boolean completed;
// Getters and Setters
}
Knowing what the data looks like beforehand helps you create a class that matches the expected structure, allowing Spring to properly serialize and deserialize the data when interacting with the API.
To expose the service methods to the clients of your application, create a new controller class:
@RestController
@RequestMapping("/api/todos")
public class TodoController {
private final TodoService todoService;
@Autowired
public TodoController(TodoService todoService) {
this.todoService = todoService;
}
@GetMapping
public List<Todo> getAllTodos() {
return todoService.getAllTodos();
}
@GetMapping("/{id}")
public Todo getTodoById(@PathVariable int id) {
return todoService.getTodoById(id);
}
}
The @RequestMapping
annotation specifies the base URL for the TodoController
, which is /api/todos
. This means that all methods in this controller will handle requests that start with this path. Combined with @RestController
, which designates the class for handling RESTful requests and ensures the response is serialized to JSON, these annotations work together to route HTTP requests to the correct methods.
If you are running the application locally on the default Spring Boot port 8080, you can expect to receive a list of todos from the JSONPlaceholder API by navigating to http://localhost:8080/api/todos
in the browser.
2. WebClient: Reactive and Modern
WebClient is a modern, non-blocking alternative to RestTemplate, introduced in Spring 5. It’s well-suited for applications that handle multiple tasks at once, such as frequent HTTP requests, without needing to wait for each to finish before moving on. To use WebClient, add the spring-boot-starter-webflux
dependency to your pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
Unlike blocking operations, which wait for each task to complete before starting the next, non-blocking operations allow an app to keep working while awaiting responses. For instance, when a request is made using WebClient
, it might trigger an I/O operation, like fetching data from an API. Instead of waiting idly for the data, the thread can switch to process a new incoming request, allowing the application to stay responsive.
Once the original request receives its response, the response data is handled as a stream of elements in a reactive, non-blocking way. This keeps requests efficient, especially when multiple requests need to be processed concurrently, as WebClient
allows each task to progress without waiting in line.
In a WebClient
setup, Mono
and Flux
are two key types for handling responses. A Mono
represents a single item, like one Todo entry, while Flux
handles a stream of items, making it ideal for lists of Todos.
Here’s an example TodoService
that calls the JSONPlaceholder API with WebClient
:
@Service
public class TodoService {
private final WebClient webClient;
@Autowired
public TodoService(WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder.baseUrl("https://jsonplaceholder.typicode.com").build();
}
public Mono<Todo> getTodoById(int id) {
return webClient.get()
.uri("/todos/{id}", id)
.retrieve()
.bodyToMono(Todo.class);
}
public Flux<Todo> getAllTodos() {
return webClient.get()
.uri("/todos")
.retrieve()
.bodyToFlux(Todo.class);
}
}
In this example, the .retrieve()
call sends the request and triggers the I/O operation, while .bodyToMono()
or .bodyToFlux()
prepares the response to be handled asynchronously once it arrives, allowing the application to process the response as soon as it’s ready.
We’ll also update TodoController
to support the Mono
and Flux
responses, making the endpoints non-blocking and reactive.
@RestController
@RequestMapping("/api/todos")
public class TodoController {
private final TodoService todoService;
@Autowired
public TodoController(TodoService todoService) {
this.todoService = todoService;
}
@GetMapping
public Flux<Todo> getAllTodos() {
return todoService.getAllTodos();
}
@GetMapping("/{id}")
public Mono<Todo> getTodoById(@PathVariable int id) {
return todoService.getTodoById(id);
}
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Todo> streamTodos() {
return todoService.getAllTodos()
.take(5)
.delayElements(Duration.ofSeconds(1)); // Simulate a 1-second delay for each Todo
}
}
In this code snippet, when a user accesses the /api/todos
endpoint, it returns all Todo
items at once after fetching them from the API. In contrast, the /api/todos/stream
endpoint uses delayElements()
to emit each of the first five Todos
with a 1-second interval. This non-blocking approach allows the application to provide data incrementally, enhancing responsiveness for the user.
The produces = MediaType.TEXT_EVENT_STREAM_VALUE
in /api/todos/stream
indicates that the endpoint returns a stream of data formatted as Server-Sent Events (SSE). This allows the client to receive updates in real time, which is especially useful for applications that require continuous data flow without refreshing the page.
3. RestTemplate: Traditional but Being Phased Out
RestTemplate
is a widely used tool for making blocking HTTP calls. While it remains popular, it is currently in maintenance mode, and Spring recommends using WebClient for new projects. Nonetheless, RestTemplate
can still be useful in many traditional applications.
To use RestTemplate
, add the spring-boot-starter-web
dependency to your pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Next, create a service class to handle API interactions:
@Service
public class TodoService {
private final RestTemplate restTemplate;
@Autowired
public TodoService(RestTemplateBuilder restTemplateBuilder) {
this.restTemplate = restTemplateBuilder.build();
}
public Todo getTodoById(int id) {
String url = "https://jsonplaceholder.typicode.com/todos/" + id;
return restTemplate.getForObject(url, Todo.class);
}
public List<Todo> getAllTodos() {
String url = "https://jsonplaceholder.typicode.com/todos";
Todo[] todos = restTemplate.getForObject(url, Todo[].class);
return Arrays.asList(todos);
}
}
In this example, the TodoService
class uses RestTemplate
to fetch data from the JSONPlaceholder API. The getTodoById()
method retrieves a specific Todo
item, while the getAllTodos()
method returns a list of all Todo
items. While straightforward, it is advisable to transition to WebClient
for new development due to its reactive features and modern design.
Conclusion
In conclusion, implementing and consuming REST endpoints in Spring Boot involves creating REST controllers for requests, using services for business logic, and applying effective error handling. For consumption, OpenFeign
simplifies client creation, while WebClient
offers a modern, non-blocking approach. Understanding these tools enables developers to build robust applications that communicate effectively over HTTP.