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

  1. Open Spring Initializr in your browser.

  2. 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

  3. 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

  1. 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.

  2. 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

  1. 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.

  2. 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:

  1. Implement a POST request to create a player:

    @PostMapping
    public String createPlayer(@RequestBody String name) {
         return "Player " + name + " created";
    }
  2. Add a GET request to return a specific player:

    @GetMapping("/{name}")
    public String readPlayer(@PathVariable String name) {
         return name;
    }
  3. Add a DELETE request to delete a player:

    @DeleteMapping("/{name}")
    public String deletePlayer(@PathVariable String name) {
         return "Player " + name + " deleted";
    }
  4. 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

  1. 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

  1. 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

  1. 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

  1. 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

  1. 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

  1. 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

  1. 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:

  1. 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"
    }
  2. Implement Not Found Handler
    To manage this error more consistently, add a method named notFoundHandler in the PlayerController class to handle NotFoundException errors:

    @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Not found")
    @ExceptionHandler(NotFoundException.class)
    public void notFoundHandler() {
    }
  3. Implement Already Exists Handler
    Next, add another method named alreadyExistsHandler to manage AlreadyExistsException errors:

    @ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Already exists")
    @ExceptionHandler(AlreadyExistsException.class)
    public void alreadyExistsHandler() {
    }
  4. Run the Application
    Open a terminal in the application root folder and execute the following command to run the application:

    ./mvnw spring-boot:run
  5. 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:

  1. 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 {
      }
  2. Define MockMvc:

    • Add a field of type MockMvc and annotate it with @Autowired:

      @Autowired
      private MockMvc mvc;
  3. Mock the FootballService:

    • Create another field of type FootballService and annotate it with @MockBean.

  4. 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.

  5. 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);
  6. 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();
  7. 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());
  8. 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 {
      }
  9. 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"));
  10. 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());
  11. 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

  1. Open the pom.xml file of your RESTful API project.

  2. 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

  1. Execute your application and navigate to the following URLs in your browser:

Step 3: Interact with Your API

  1. 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:

  1. 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.

  2. Download and Set Up the Project:

    • Generate the project, download the ZIP file, and extract it.

    • Open the pom.xml file.

  3. 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) {
      }
  4. 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();
      }
  5. 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();
          }
      }
  6. Modify the AlbumsApplication Class:

    • Update the AlbumsApplication class by adding the @EnableFeignClients annotation:

      @EnableFeignClients
      @SpringBootApplication
      public class AlbumsApplication {
      }
  7. 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.

  8. 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:

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

  1. 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

  2. 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.
  3. 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;
          }
      }
  4. 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>>() {});
      }
  5. 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));
              });
      }
  6. 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 using bodyTo(), passing the Player 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

  1. Create a test method named getPlayerTest:

@Test
public void getPlayerTest() {
  1. 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"
        }
    """)));
  1. Call the getPlayer method:

Optional<Player> player = footballClientService.getPlayer("325636");
  1. 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.