Creating a RESTful API
A RESTful API is a standardized way for software components to communicate over the internet using HTTP methods and URLs. It is fundamental for modern web and cloud application development, promoting scalable, flexible, and stateless communication. Understanding RESTful APIs is crucial for building and integrating services.
In this chapter, you’ll create a system to manage a football card-trading game, encompassing teams, players, albums, and cards. Specifically, you will create a RESTful API that exposes Create, Read, Update, and Delete (CRUD) operations for football players.
Getting Started
To create a RESTful API, we’ll use Spring Boot and a tool named Spring Initializr. Open the tool in your browser by navigating to https://start.spring.io/. This tool helps you generate a Spring Boot project with all necessary dependencies. It integrates well with code editors like VSCode and IntelliJ (Premium edition).
Step-by-Step Instructions
Follow these steps to create a RESTful project using Spring Initializr and establish your first endpoint with typical HTTP operations:
1. Configure Your Project
-
Open Spring Initializr in your browser.
-
Set the following configuration:
-
Project: Select Maven
-
Language: Select Java
-
Spring Boot: Choose the latest stable version (at the time of writing, it’s 3.1.4)
-
Dependencies: Select Spring Web
-
Project Metadata:
-
Group:
com.packt
-
Artifact:
football
-
Name: Leave autogenerated
-
Package name: Leave autogenerated
-
Description: Enter a description like Demo project for Spring Boot 3 Cookbook
-
Packaging: Select Jar
-
Java: Select 21
-
-
-
After configuring the options, choose one of the following:
-
Explore: To explore the project before downloading.
-
Share: Generates a URL to share your configuration (e.g., this example).
-
Generate: Click this option to download a ZIP file of the project structure.
-
2. Set Up Your Project
-
Unzip the downloaded file. You now have the basic project structure, although no API exists yet. If you run the application now, you’ll receive an HTTP 404 Not Found response.
-
In the src/main/java/com/packt/football folder, create a file named PlayerController.java with the following content to create a RESTful endpoint:
package com.packt.football; import java.util.List; import org.springframework.web.bind.annotation.*; @RequestMapping("/players") @RestController public class PlayerController { @GetMapping public List<String> listPlayers() { return List.of("Ivana ANDRES", "Alexia PUTELLAS"); } }
3. Run the Application
-
Open a terminal in the project root folder and execute the following command:
./mvnw spring-boot:run
This command builds your project and starts the application. By default, the web container listens on port 8080.
-
To test the endpoint, execute an HTTP request. You can either open http://localhost:8080/players in a browser or use
curl
:curl http://localhost:8080/players
You should see a list of players returned by the controller.
4. Enhance Your RESTful API
Add more HTTP verbs to your RESTful endpoint by modifying the PlayerController.java file:
-
Implement a POST request to create a player:
@PostMapping public String createPlayer(@RequestBody String name) { return "Player " + name + " created"; }
-
Add a GET request to return a specific player:
@GetMapping("/{name}") public String readPlayer(@PathVariable String name) { return name; }
-
Add a DELETE request to delete a player:
@DeleteMapping("/{name}") public String deletePlayer(@PathVariable String name) { return "Player " + name + " deleted"; }
-
Implement a PUT request to update a player’s name:
@PutMapping("/{name}") public String updatePlayer(@PathVariable String name, @RequestBody String newName) { return "Player " + name + " updated to " + newName; }
5. Test the Enhanced API
-
Restart your application as explained in the previous step and test the new endpoints using
curl
commands:-
GET a specific player:
curl http://localhost:8080/players/Ivana%20ANDRES
-
POST a new player:
curl --header "Content-Type: application/text" --request POST --data 'Itana BONMATI' http://localhost:8080/players
-
PUT to update a player:
curl --header "Content-Type: application/text" --request PUT --data 'Aitana BONMATI' http://localhost:8080/players/Itana%20BONMATI
-
DELETE a player:
curl --header "Content-Type: application/text" --request DELETE http://localhost:8080/players/Aitana%20BONMATI
-
You should receive appropriate responses for each operation.
Understanding How It Works
By adding the Spring Web dependency, Spring Boot incorporates a Tomcat server into the application. Tomcat is a popular open-source web server and servlet container for Java-based applications. The application listens on port 8080 by default.
-
Annotations:
-
@RestController: Flags the class as a RESTful controller.
-
@RequestMapping: Maps HTTP requests to handler methods.
-
@GetMapping, @PostMapping, @PutMapping, @DeleteMapping: Specializations of
@RequestMapping
for handling respective HTTP methods. -
@PathVariable: Maps part of the URL to the method parameter.
-
@RequestBody: Maps the request body to the method parameter.
-
These annotations allow Spring Boot to handle requests seamlessly.
Further Learning
While the RESTful endpoint created is simple, it covers essential CRUD operations. Understanding HTTP status codes and proper response management is vital for effective API design.
For more information on API design best practices, consider visiting:
Defining Responses and the Data Model Exposed by the API
In the previous recipe, we created a simple RESTful API. To enhance the user experience for consumers of the API, we must incorporate standard response codes and a consistent data model. In this recipe, we will improve the RESTful API by returning standard response codes and defining a data model for our players endpoint.
How to Do It…
We’ll create a structured folder hierarchy to organize different class types. Additionally, we will define a data model to expose in our RESTful API and create a service that provides the necessary operations for the API.
All content created in the following steps will be located under the src/main/java/com/packt/football folder or one of its subfolders. Let’s get started:
Step 1: Create the Data Model
-
Create a Folder for the Model
-
Create a folder named model.
-
Inside this folder, create a file named Player.java with the following content:
public record Player(String id, int jerseyNumber, String name, String position, LocalDate dateOfBirth) { }
-
Step 2: Create Custom Exceptions
-
Create a Folder for Exceptions
-
Create a folder named exceptions.
-
Inside this folder, create two files:
-
AlreadyExistsException.java:
package com.packt.football.exceptions; public class AlreadyExistsException extends RuntimeException { public AlreadyExistsException(String message) { super(message); } }
-
NotFoundException.java:
package com.packt.football.exceptions; public class NotFoundException extends RuntimeException { public NotFoundException(String message) { super(message); } }
-
Step 3: Create the Service
-
Create a Folder for Services
-
Create a folder named services.
-
Inside this folder, create a class named FootballService. This class will manage all operations needed by our RESTful API. Start by defining the class:
import org.springframework.stereotype.Service; @Service public class FootballService { private final Map<String, Player> players = Map.ofEntries( Map.entry("1884823", new Player("1884823", 5, "Ivana ANDRES", "Defender", LocalDate.of(1994, 07, 13))), Map.entry("325636", new Player("325636", 11, "Alexia PUTELLAS", "Midfielder", LocalDate.of(1994, 02, 04))) ); // List players public List<Player> listPlayers() { return new ArrayList<>(players.values()); } // Get a player by ID public Player getPlayer(String id) { Player player = players.get(id); if (player == null) { throw new NotFoundException("Player not found"); } return player; } // Add a new player public Player addPlayer(Player player) { if (players.containsKey(player.id())) { throw new AlreadyExistsException("The player already exists"); } else { players.put(player.id(), player); return player; } } // Update an existing player public Player updatePlayer(Player player) { if (!players.containsKey(player.id())) { throw new NotFoundException("The player does not exist"); } else { players.put(player.id(), player); return player; } } // Delete a player public void deletePlayer(String id) { players.remove(id); } }
-
Step 4: Modify the Player Controller
-
Update the PlayerController
-
In the PlayerController class, modify the controller to utilize the new service and expose the newly created data model:
import org.springframework.web.bind.annotation.*; @RequestMapping("/players") @RestController public class PlayerController { private final FootballService footballService; public PlayerController(FootballService footballService) { this.footballService = footballService; } @GetMapping public List<Player> listPlayers() { return footballService.listPlayers(); } @GetMapping("/{id}") public Player readPlayer(@PathVariable String id) { return footballService.getPlayer(id); } @PostMapping public void createPlayer(@RequestBody Player player) { footballService.addPlayer(player); } @PutMapping("/{id}") public void updatePlayer(@PathVariable String id, @RequestBody Player player) { footballService.updatePlayer(player); } @DeleteMapping("/{id}") public void deletePlayer(@PathVariable String id) { footballService.deletePlayer(id); } }
-
Step 5: Run the Application
-
Start the Application
-
In the application root folder, open a terminal and execute the following command to run the application:
./mvnw spring-boot:run
-
Step 6: Test the Application
-
Verify Functionality Using curl
-
Test the application by executing the following curl command to retrieve all players:
curl http://localhost:8080/players
Expected output:
[{"id":"325636","jerseyNumber":11,"name":"Alexia PUTELLAS","position":"Midfielder","dateOfBirth":"1994-02-04"}, {"id":"1884823","jerseyNumber":5,"name":"Ivana ANDRES","position":"Defender","dateOfBirth":"1994-07-13"}]
-
How It Works…
In this recipe, we defined a new record type named Player. Spring Boot automatically serializes this object into a response body that can be sent to the client in formats such as JSON or XML.
About Records
The record feature was introduced in Java 16 and provides a convenient way to declare classes that serve as simple data carriers, automatically generating methods like equals(), hashCode(), and toString() based on the record components. This feature simplifies the creation of classes that primarily encapsulate data. Spring Boot 3 requires Java 17 or higher.
If you have special serialization requirements, you can configure your own message converter by implementing WebMvcConfigurer and overriding the configureMessageConverters method. For more information, refer to the Spring Framework documentation.
HTTP Status Codes Handling
Spring Boot’s default handling of HTTP status codes can be summarized as follows:
-
Successful execution returns an HTTP 200 status.
-
Unimplemented methods return a 405 Method Not Allowed error.
-
Requests for non-existent resources yield a 404 Not Found status.
-
Invalid requests return a 400 Bad Request status.
-
In case of exceptions, an HTTP 500 Internal Server Error status is returned.
-
Security-related operations may yield 401 Unauthorized or 403 Forbidden statuses.
While this behavior may suffice in some scenarios, providing proper semantics to your RESTful API is advisable. The next recipe will discuss how to handle these scenarios more effectively.
Note that the FootballService class is annotated with @Service, which registers it as a Spring bean and makes it available in the IoC container. When a Spring Boot application starts, it scans for classes annotated with various stereotypes, such as @Service, @Controller, and @Bean. Consequently, when Spring Boot instantiates the PlayerController, it passes an instance of the FootballService.
Managing Errors in a RESTful API
In the previous recipe, we enhanced our RESTful API by using complex data structures. However, the application wasn’t able to handle common errors or return standard response codes. In this recipe, we will improve the API by managing common errors and returning consistent response codes according to standard practices.
How to Do It
In this recipe, we will modify the RESTful API created previously to handle exceptions and return appropriate HTTP response codes. All content created in the following steps will be located under the src/main/java/com/packt/football folder or its subfolders. Let’s get started:
-
Handle Non-Existing Players
Attempting to retrieve a non-existing player or creating the same player twice will throw an exception, resulting in an HTTP 500 server error:curl http://localhost:8080/players/99999
Example response:
{ "timestamp": "2023-09-16T23:18:41.906+00:00", "status": 500, "error": "Internal Server Error", "path": "/players/99999" }
-
Implement Not Found Handler
To manage this error more consistently, add a method namednotFoundHandler
in the PlayerController class to handleNotFoundException
errors:@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Not found") @ExceptionHandler(NotFoundException.class) public void notFoundHandler() { }
-
Implement Already Exists Handler
Next, add another method namedalreadyExistsHandler
to manageAlreadyExistsException
errors:@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Already exists") @ExceptionHandler(AlreadyExistsException.class) public void alreadyExistsHandler() { }
-
Run the Application
Open a terminal in the application root folder and execute the following command to run the application:./mvnw spring-boot:run
-
Test the Application
Execute the following curl commands to test the error handling:-
To get a player that does not exist:
curl http://localhost:8080/players/99999
Example response:
{ "timestamp": "2023-09-16T23:21:39.936+00:00", "status": 404, "error": "Not Found", "path": "/players/99999" }
This indicates that our application adheres to standard RESTful API semantics.
-
Verify handling of the
AlreadyExistsException
by executing the following request to create a player twice:data="{'id': '8888', 'jerseyNumber':6, 'name':'Cata COLL', 'position':'Goalkeeper', 'dateOfBirth': '2001-04-23'}" curl --header "Content-Type: application/json" --request POST --data "$data" http://localhost:8080/players
The first request will succeed with an HTTP 200 response, while the second request will return an HTTP 400 response.
-
How It Works
Spring Boot manages HTTP status codes for common cases. This recipe
demonstrates how to handle application-specific scenarios requiring
consistent HTTP status codes. A 404 status code should be returned
when a resource is not found. Instead of returning a null value from the
service, which would result in an HTTP 200 response, we use the
@ExceptionHandler
annotation to define a handler for specific
exceptions and the @ResponseStatus
annotation to specify the HTTP
status code.
There’s More…
You can control response codes more explicitly by returning a
ResponseEntity instead of your data model directly in the controller.
For instance, here’s how you can implement the getPlayer
method:
@GetMapping("/{id}")
public ResponseEntity<Player> readPlayer(@PathVariable String id) {
try {
Player player = footballService.getPlayer(id);
return new ResponseEntity<>(player, HttpStatus.OK);
} catch (NotFoundException e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
Alternatively, you can create a global handler for all controllers by
using a class annotated with @ControllerAdvice
:
package com.packt.football;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<String> handleGlobalException(NotFoundException ex) {
return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
}
}
Using this approach allows for consistent error handling across all RESTful endpoints in your application.
Testing a RESTful API
Testing applications manually can be tedious, especially when handling complex scenarios that are difficult to validate. Furthermore, manual testing lacks scalability in terms of development productivity. Therefore, I highly recommend implementing automated testing.
Spring Boot includes the Testing starter by default, which provides essential components for unit and integration testing. In this guide, we will learn how to implement a unit test for our RESTful API.
How to Do It
Let’s add some tests to our RESTful API to ensure our application behaves correctly whenever we make changes:
-
Create a New Test Class:
-
In the src/test folder, create a new test class named PlayerControllerTest and annotate it with @WebMvcTest:
@WebMvcTest(value = PlayerController.class) public class PlayerControllerTest { }
-
-
Define MockMvc:
-
Add a field of type MockMvc and annotate it with @Autowired:
@Autowired private MockMvc mvc;
-
-
Mock the FootballService:
-
Create another field of type FootballService and annotate it with @MockBean.
-
-
Write the First Test:
-
Create a method named testListPlayers to validate the behavior of our RESTful API when returning the list of players:
@Test public void testListPlayers() throws Exception { }
-
Ensure this method is annotated with @Test.
-
-
Configure the FootballService:
-
Inside the testListPlayers method, configure the FootballService to return a list of two players when invoking the listPlayers method:
Player player1 = new Player("1884823", 5, "Ivana ANDRES", "Defender", LocalDate.of(1994, 07, 13)); Player player2 = new Player("325636", 11, "Alexia PUTELLAS", "Midfielder", LocalDate.of(1994, 02, 04)); List<Player> players = List.of(player1, player2); given(footballService.listPlayers()).willReturn(players);
-
-
Emulate HTTP Calls:
-
Use the mvc field to perform the HTTP GET request and validate its behavior:
MvcResult result = mvc.perform(MockMvcRequestBuilders .get("/players") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$", hasSize(2))) .andReturn();
-
-
Perform Additional Validations:
-
Validate that the returned array of players is as expected:
String json = result.getResponse().getContentAsString(); ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); List<Player> returnedPlayers = mapper.readValue(json, mapper.getTypeFactory().constructCollectionType(List.class, Player.class)); assertArrayEquals(players.toArray(), returnedPlayers.toArray());
-
-
Test Error Management:
-
Create a new method named testReadPlayer_doesnt_exist in the PlayerControllerTest class and annotate it with @Test:
@Test public void testReadPlayer_doesnt_exist() throws Exception { }
-
-
Configure Error Handling:
-
Arrange the getPlayer method of the FootballService to throw a NotFoundException for a non-existent player:
String id = "1884823"; given(footballService.getPlayer(id)).willThrow(new NotFoundException("Player not found"));
-
-
Simulate the Request and Validate:
-
Use the mvc field to simulate the request and validate the expected behavior:
mvc.perform(MockMvcRequestBuilders.get("/players/" + id).accept(MediaType.APPLICATION_JSON)) .andExpect(status().isNotFound());
-
-
Execute the Tests:
-
Run the tests using the following command:
mvn test
-
The test goal is automatically executed whenever you run the package or install goals unless explicitly disabled. You can also execute the tests from your preferred IDE.
-
How It Works
By default, Spring Initializr includes a dependency for spring-boot-starter-test, which provides all necessary components for testing. Here are some key elements used in this recipe:
-
@WebMvcTest: This annotation configures the testing class to focus only on MVC components, disabling default Spring Boot autoconfiguration.
-
@MockBean: This annotation allows you to create a mock implementation of FootballService, replacing any existing bean registration.
-
given: This method helps specify behavior for mocked methods, such as setting return values.
-
MockMvc: This simulates web server behavior, enabling you to test controllers without deploying the application.
In this recipe, we also utilized JUnit utilities like assertArrayEquals to compare arrays.
See Also
In this book, I apply the Arrange-Act-Assert (AAA) principles when writing tests:
-
Arrange: Set up the conditions needed for the test.
-
Act: Perform the action being tested.
-
Assert: Verify that the expected results were achieved.
Additionally, the Arrange-Act-Assert-Clean (AAAC) variant includes a cleanup step, which ideally shouldn’t be necessary if you mock components or services that require cleanup.
Using OpenAPI to Document Our RESTful API
Now that we have a RESTful API, we can create a consumer application. Instead of simply performing HTTP requests and requiring consumers to understand our application’s codebase, we can use OpenAPI. OpenAPI is a standard for documenting RESTful APIs and can generate client applications across different languages and frameworks. Spring Boot has excellent support for OpenAPI.
In this guide, we’ll learn how to add OpenAPI support to our RESTful API and use the tools it provides for consumption.
OpenAPI 3.0 is the new name for Swagger after it was donated by SmartBear to the OpenAPI Initiative. Many resources may still refer to OpenAPI as Swagger. |
How to Do It
Let’s document our RESTful API with OpenAPI and start testing it using the OpenAPI user interface.
Step 1: Add OpenAPI Dependency
-
Open the pom.xml file of your RESTful API project.
-
Add the SpringDoc OpenAPI Starter WebMVC UI dependency by inserting the following XML into the
<dependencies>
element:<dependencies> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.2.0</version> </dependency> </dependencies>
For brevity, I have removed other dependencies from the code snippet, but ensure you keep all of them in your code.
Step 2: Run the Application
-
Execute your application and navigate to the following URLs in your browser:
-
http://localhost:8080/v3/api-docs: This URL returns the description of your RESTful API in OpenAPI format.
-
http://localhost:8080/swagger-ui/index.html: This URL provides a user-friendly interface to interact with your API.
-
Step 3: Interact with Your API
-
You will see that all the RESTful operations defined in your application and the data model (e.g., Player) are exposed. You can execute any of the available operations directly from the browser.
How It Works
The org.springdoc:springdoc-openapi-starter-webmvc-ui dependency inspects your application at runtime to generate the descriptions of the available endpoints. The core of OpenAPI is the service definition found at http://localhost:8080/v3/api-docs. This JSON document follows the OpenAPI schema and describes the RESTful endpoints hosted in your application, including paths, HTTP methods, parameters, responses, and data schemas.
Additionally, the OpenAPI dependency provides a user-friendly UI that
utilizes the OpenAPI schema, allowing for easy interaction with the
service. This can serve as an alternative to using curl
for testing
the RESTful service, eliminating the need to memorize all possible
arguments.
Consuming a RESTful API from Another Spring Boot Application Using FeignClient
In this guide, we will learn how to create a consumer application for a RESTful API using FeignClient. While there are various tools available to generate client code from the OpenAPI specification, we will manually create the client code for educational purposes.
We will create a new Spring Boot application using the Spring Initializr tool: Spring Initializr.
Steps to Create the Consumer Application
We’ll create a Spring Boot application that consumes the Football RESTful API from the previous recipe:
-
Create a New Spring Boot Application:
-
Open Spring Initializr.
-
Use the same parameters as in the Creating a RESTful API recipe, but modify the following options:
-
Artifact:
albums
-
Dependencies: Select Spring Web and OpenFeign.
-
-
-
Download and Set Up the Project:
-
Generate the project, download the ZIP file, and extract it.
-
Open the pom.xml file.
-
-
Create the
Player
Record:-
Create a new record named
Player
with the following code:public record Player(String id, Integer jerseyNumber, String name, String position, LocalDate dateOfBirth) { }
-
-
Create the
FootballClient
Interface:-
Create an interface named
FootballClient
and add the following code:@FeignClient(name = "football", url = "http://localhost:8080") public interface FootballClient { @RequestMapping(method = RequestMethod.GET, value = "/players") List<Player> getPlayers(); }
-
-
Create the
AlbumsController
:-
Create a controller named
AlbumsController.java
with the following code:@RestController @RequestMapping("/albums") public class AlbumsController { private final FootballClient footballClient; public AlbumsController(FootballClient footballClient) { this.footballClient = footballClient; } @GetMapping("/players") public List<Player> getPlayers() { return footballClient.getPlayers(); } }
-
-
Modify the
AlbumsApplication
Class:-
Update the
AlbumsApplication
class by adding the@EnableFeignClients
annotation:@EnableFeignClients @SpringBootApplication public class AlbumsApplication { }
-
-
Run the Application:
-
Execute the application by running the following command in your terminal:
./mvnw spring-boot:run -Dspring-boot.run.arguments=--server.port=8081
This command runs the application on port 8081 to avoid conflicts with the existing API running on port 8080.
-
-
Test the Application:
-
Use the following curl command to test the application:
curl http://localhost:8081/albums/players
You should receive a response similar to this:
[{"id":"1884823","jerseyNumber":5,"name":"Ivana ANDRES","position":"Defender","dateOfBirth":"1994-07-13"}, {"id":"325636","jerseyNumber":11,"name":"Alexia PUTELLAS","position":"Midfielder","dateOfBirth":"1994-02-04"}]
-
Explanation
Feign is a declarative web service client framework that simplifies the process of making HTTP requests to RESTful web services. To create a Feign client, define an interface that specifies the HTTP requests you want to make to a particular service. The methods in this interface are annotated with HTTP method annotations like @RequestMapping, @GetMapping, @PostMapping, etc., to specify the HTTP method and the URL path.
By injecting the Feign client interface into your Spring components, you
can make HTTP requests effortlessly. Spring Cloud Feign automatically
generates and executes the HTTP requests based on your interface
definition. The @EnableFeignClients
annotation in the application
class allows the framework to scan for interfaces with the
@FeignClient annotation and generate the necessary client code.
In the controller, you can then utilize the Feign client via Spring Boot dependency injection.
Additional Information
While Feign is used in this example for its seamless integration with Spring Cloud components (like Eureka Server), there are other alternatives available for making HTTP requests. The manual approach we took is beneficial for learning, but in practice, tools can help maintain client-side code in sync with server descriptions. Here are two useful tools:
-
OpenAPI Generator: OpenAPITools
-
Swagger Codegen: swagger-api/swagger-codegen
Both tools offer command-line interfaces and Maven plugins for generating client-side code based on OpenAPI descriptions.
Consuming a RESTful API from Another Spring Boot Application Using RestClient
In this guide, we will leverage the new RestClient
component
introduced in Spring Framework 6.1 and available in Spring Boot starting
from version 3.2. This approach provides a fluent API that abstracts
HTTP libraries, enabling easy conversion between Java objects and HTTP
requests/responses.
Steps to Create the Application
-
Create a New Spring Boot Application:
-
Open Spring Initializr in your browser.
-
Use the same parameters as in the Creating a RESTful API recipe, but update the following:
-
For Artifact, enter albums
-
For Dependencies, select Spring Web
-
-
-
Create a Configuration Class:
-
Create a configuration class named AlbumsConfiguration. In this class, define a
RestClient
bean:@Configuration public class AlbumsConfiguration { @Value("${football.api.url:http://localhost:8080}") String baseURI; @Bean RestClient restClient() { return RestClient.create(baseURI); } }
The @Value
annotation allows you to configure the URL of the remote server.
-
-
Create a Service Class:
-
Create a service class named FootballClientService. This class will inject the
RestClient
bean via the constructor:@Service public class FootballClientService { private final RestClient restClient; public FootballClientService(RestClient restClient) { this.restClient = restClient; } }
-
-
Retrieve Data from the Remote API:
-
Implement a method named getPlayers to retrieve a list of players:
public List<Player> getPlayers() { return restClient.get().uri("/players").retrieve() .body(new ParameterizedTypeReference<List<Player>>() {}); }
-
-
Retrieve a Single Player:
-
Create a method to get a single player by ID:
public Optional<Player> getPlayer(String id) { return restClient.get().uri("/players/{id}", id) .exchange((request, response) -> { if (response.getStatusCode().equals(HttpStatus.NOT_FOUND)) { return Optional.empty(); } return Optional.of(response.bodyTo(Player.class)); }); }
-
-
Create an Album RESTful API:
-
Finally, implement an Album RESTful API using the FootballClientService. A sample version is available in the book’s GitHub repository.
-
How It Works
This recipe utilizes the RestClient
without creating additional
types to replicate the remote RESTful API. The RestClient
allows us
to perform requests using a fluent API style, creating readable and
maintainable code.
Detailed Explanation of getPlayer
-
We start with the
get()
method, which initializes the request properties (e.g., URI, headers). -
The
uri()
method sets the endpoint, which appends to the base URL defined in AlbumsConfiguration. -
The
exchange()
method performs the call and provides a handler for the response. -
In the response handler, we handle the case where the player is not found by returning an empty
Optional
. Otherwise, we deserialize the response usingbodyTo()
, passing thePlayer
class.
The getPlayers
method is similar but returns a List<Player>
. To
specify the generic type, we use ParameterizedTypeReference
,
creating an inline subclass for deserialization.
Configuration Properties
We use the @Value
annotation in AlbumsConfiguration to inject
values from external sources (e.g., configuration files, environment
variables). The expression ${football.api.url:http://localhost:8080}
means it will attempt to read the football.api.url
property first;
if not found, it defaults to http://localhost:8080
.
The format of properties will differ based on whether they are in application.properties or application.yml:
-
application.properties:
football.api.url=http://localhost:8080
-
application.yml:
football: api: url: http://localhost:8080
In this book, most examples will use the application.yml format, though application.properties may also appear, especially in the context of environment variables.
Mocking a RESTful API with WireMock
In this guide, we’ll learn how to mock a remote RESTful API service using WireMock. Mocking is helpful as it allows you to test your application in isolation, without needing the actual remote service to be available. This is particularly useful for handling unreliable or slow services, or when you want to simulate various scenarios that may be hard to recreate with the actual service.
Steps to Mock the API
Follow these steps to set up WireMock for mocking the remote Football service in your Albums application:
1. Add WireMock Dependency
Open your pom.xml file and add the following dependency to include WireMock in your project:
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>3.0.1</version>
<scope>test</scope>
</dependency>
2. Create Test Class
Create a new test class named FootballClientServiceTest. Use the @SpringBootTest annotation to specify a property for the remote server address:
@SpringBootTest(properties = { "football.api.url=http://localhost:7979" })
public class FootballClientServiceTests {
}
3. Set Up WireMock Server
Inside your FootballClientServiceTest class, set up the WireMock server:
private static WireMockServer wireMockServer;
@BeforeAll
static void init() {
wireMockServer = new WireMockServer(7979);
wireMockServer.start();
WireMock.configureFor(7979);
}
4. Autowire FootballClientService
Declare a field for FootballClientService and annotate it with @Autowired:
@Autowired
FootballClientService footballClientService;
5. Write a Test for getPlayer
-
Create a test method named getPlayerTest:
@Test
public void getPlayerTest() {
-
Arrange the expected response from the remote service by stubbing it:
WireMock.stubFor(WireMock.get(WireMock.urlEqualTo("/players/325636"))
.willReturn(WireMock.aResponse()
.withHeader("Content-Type", "application/json")
.withBody("""
{
"id": "325636",
"jerseyNumber": 11,
"name": "Alexia PUTELLAS",
"position": "Midfielder",
"dateOfBirth": "1994-02-04"
}
""")));
-
Call the getPlayer method:
Optional<Player> player = footballClientService.getPlayer("325636");
-
Validate the results:
Player expectedPlayer = new Player("325636", 11, "Alexia PUTELLAS", "Midfielder", LocalDate.of(1994, 2, 4));
assertEquals(expectedPlayer, player.get());
6. Additional Tests
As an exercise, consider creating tests for other methods of the FootballClientService class. You can also simulate different server responses. More example tests can be found in the book’s GitHub repository.
How It Works
WireMock is a powerful library for API mock testing and can be used standalone or integrated into a project. In this recipe, we added it as a test dependency to avoid conflicts with the production environment.
We configured the WireMock server to listen on port 7979, which matches the property we set in the @SpringBootTest annotation. This ensures that the tests can run without interference from any real remote services.
The StubFor
method allows us to define the expected behavior of the
mock server, enabling us to test our application logic against
predefined responses.
Further Reading
For more information on WireMock, visit the official WireMock website.