1 Mar 2023 · Software Engineering

    Why You Need an API Layer in Your Spring Boot App and How to Implement It

    16 min read
    Contents

    A microservices architecture consists of several independent backend services that communicate with each other through APIs. Given the popularity of the microservices approach, chances are that your backend needs to make several API calls to receive data from and send data to other backends.

    Making an HTTP request to call an API takes only a few lines of code. However, you likely need to call the same API in different places. This implies repeating the same logic over and over again. Such an approach is cumbersome and leads to code duplication. Just think about when the endpoint of an API changes. In this common scenario, you will have to update the code at all points where the API is called.

    Now, imagine that your backend software architecture had a layer to contain everything required to call all APIs. Centralizing all the code to make API calls in one place would provide several benefits and help mitigate the negative aspects mentioned earlier. This is what an API layer is all about!

    Let’s figure out what an API layer is, understand why your backend app should have one, and learn how to integrate an API layer into your Spring Boot backend.

    What is an API Layer?

    An API layer is the part of the backend Application that encapsulates all the Programming logic required to receive and send data through an Interface (API). In other words, all external API calls a backend application relies on pass through this architectural layer. Keep in mind that this is nothing new, and you can take advantage of an API Layer also in a frontend application. Find out more about how to build an API Layer in React.

    In a Spring Boot application, all the logic required to implement the API layer should be placed in an api package. Then, you can use the following format to name the Java class files contained in that package:

    <ExternalService>API.java

    This Java file should expose all API calls related to a particular external microservice as functions. Thus, the api package will consist of several classes. Each of these files will collect all APIs related to the service identified with the <ExternalService> qualifier. This will also make finding APIs of the same type more intuitive.

    Keep in mind that a backend API layer usually also requires some utility classes. These files do not follow the naming convention presented above. So, here is what an API layer in a Spring Boot application may look like:

    api
    ├── utils
    │    └── APIHandler.java    
    │
    ├── APILayer.java
    .
    .
    .
    ├── AuthorizationAPI.java
    .
    .
    .
    ├── CmsAPI.java
    .
    .
    .
    └── SemaphoreAPI.java

    As you can see in the file diagram above, the API layer groups together all the APIs of external services that your backend application uses.

    In particular, let’s focus on the APILayer.java file. This is a special file that encapsulates all the <ExternalService>API.java files as follows:

    package com.apilayer.service.api;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    @Service
    public class APILayer {
        private final AuthorizationAPI authorizationAPI;
        private final CmsAPI cmsAPI;
        private final SemaphoreAPI semaphoreAPI;
        // other <ExternalService>API ...
    
    @Component
        APILayer(
            @Autowired AuthorizationAPI authorizationAPI,
            @Autowired CmsAPI cmsAPI,
            @Autowired SemaphoreAPI semaphoreAPI
            // ...
        ) {
            this.authorizationAPI = authorizationAPI;
            this.cmsAPI = authorizationAPI;
            this.semaphoreAPI = semaphoreAPI;
            // ...
        }
    
        public AuthorizationAPI getAuthorizationAPI() {
            return authorizationAPI;
        }
    
        public SemaphoreAPI getSemaphoreAPI() {
            return semaphoreAPI;
        }
    
        public CmsAPI getCmsAPI() {
            return cmsAPI;
        }
    
        // other getters...
    }


    APILayer is annotated with @Component because it is generally contained in the service layer. The reason for this is that retrieving and writing data via API is part of the business logic, which should be defined in the service layer.

    So, if your Spring Boot backend follows a multi-tier architecture with a package for each layer, the api package should be placed inside the service layer:

    com.your-app
    ├── controller
    │    └── ...    
    ├── dao
    │    └── ...
    ├── data
    │    └── ...
    ├── dto
    │    └── ...
    ├── model
    │    └── ...
    .
    .
    .
    └── service
         ├── api
         │    ├── utils
         │    └── ...
         └── ...

    Note the api folder under the service directory. The API layer can be considered a specialized sub-layer of the service layer.

    Thanks to APILayer, importing the entire API layer requires only one @Autowired variable. Specifically, you can now call an API in a @Service class as follows:

    package com.apilayer.service;
    
    import com.apilayer.service.api.APILayer;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import java.util.List;
    
    // ...
    
    @Service
    public class ArticleService {
        private final APILayer apiLayer;
      
        // registering the APILayer bean
        // via constructor dependency injection
        public ArticleService(@Autowired APILayer apiLayer) {
            this.apiLayer = apiLayer;
        }
    
        public List<Article> getAllArticles(
        )  {
            // retrieving some data from an external CMS 
            // through an API call
            List<Article> articles = apiLayer.getCmsAPI().getAllArticles();
    
           // remaining business logic...
          
            return articles;
        }
      
        // ...
    }

    This example shows how the API layer simplifies the process of sending and receiving data via API. In detail, the API layer provides functions that encapsulate all the logic required for performing API calls.

    Let’s now dig into the benefits an API layer can bring to your backend architecture.

    Why Your Spring Boot Application Should Have an API Layer

    An API layer offers several advantages to the architecture of a Spring Boot application. Let’s take a look at the three most important reasons why you should use it.

    1. Avoiding Code Duplication

    In the microservices architecture, backend applications use APIs to communicate with each other. So, your backend is likely to make a lot of API requests. In a Spring Boot application with a traditional architecture, this means repeating the lines of code shown below throughout your backend codebase:

    // verifying if an authorization token
    // is valid by calling an API
    // exposed by the authentication microservice
    HttpHeaders httpHeaders = new HttpHeaders();
    httpHeaders.add(HttpHeaders.AUTHORIZATION, token);
    
    boolean isValid = return restTemplate.exchange(
           "https://your-auth-backend.com/api/v1/verifyToken", 
            HttpMethod.GET, 
            new HttpEntity<>(null, httpHeaders), 
            Boolean.class
    );

    The example above shows how to call an API in Spring Boot with RestTemplate, the default Spring Boot Web HTTP client. Specifically, it demonstrates how to make an authentication API request. Since all authenticated APIs in your backend will rely on this external API call, you will have to replicate this logic multiple times. This will lead to duplication, which increases the lines of code in the codebase and thus the possibility of introducing errors and bugs. Specifically, even a single API called incorrectly could cause your application to crash, and you want to avoid this.  Plus, the number of APIs a backend depends on is generally very limited. So, duplicating some logic to call the same APIs seems inevitable with this approach.

    Now, consider centralizing all the logic behind your API calls in functions exposed by an API layer. Performing an API call would take a single line of code, the function call. Also, when an API endpoint changes, you would only need to update a single function. Without the API layer, you would have to find and change every API call made throughout the code. A huge difference!

    Therefore, an API layer makes the Spring Boot backend more modern, easier to maintain, and cleaner, without the same lines of code repeated again and again.

    2. Simplifying Integration of Third-Party Services

    Today’s most successful services are based on a composable architecture. This involves integrating best-in-class online services into a single application. After all, no one wants to reinvent the wheel, and relying on successful products makes everything easier.

    You can apply the same logic to a backend application, especially considering that most SaaS (Software as a Service) services are now API-based. This means that you can get access to their data and functionality via API.

    With an API layer, you already have the infrastructure in place to handle API calls. In particular, integrating a new third-party service into your backend simply involves defining a new <ExternalService>API.java file. To make things even simpler, you can also configure a custom RestTemplate instance as follows:

    // building a RestTemplate instance
    // with custom default headers
    RestTemplate semaphoreRestTemplate = new RestTemplateBuilder()
                .defaultHeader("Authorization", "Token: <YOUR_SEMPAHORE_API_KEY>")
                .build();

    For example, you can use this instance of RestTemplate in your SemaphoreAPI.java file. Every API call made with that instance will automatically have the Authorization HTTP header set to the desired value.

    Thanks to the API layer, integrating external services requires only a few lines of code. All the logic is centralized in one file and exposed through functions. so switching from one service to another is simple. You only have to update the code inside each function in the corresponding <ExternalService>API.java file. This saves you from major refactoring.

    3. Making It Easier to Add Retry Logic on API Failure

    No API-based SaaS offers 100% uptime. In addition, some of these external services may be error-prone, especially during updates. This means that third-party services are no strangers to server-side HTTP 5xx errors, especially the 503 Service Unavailable variety.

    When a third-party service responds with an error, your backend may fail as a result. Therefore, errors returned by any external service used by the backend are also your problem. At the same time, third-party services base their success on the effectiveness and availability of their features. So, these errors must be temporary. This is why retrying HTTP requests in case of specific errors is so important.

    Thanks to Spring Retry, you can effortlessly integrate retry logic into your Spring Boot app. All you have to do is decorate the API layer methods with the Spring Retry @Retryable annotation. These methods will then be executed up to three times, with a delay of one second between each attempt.

    Adding an API Layer to a Spring Boot App

    You now know what an API Layer is and what benefits it can bring to a Spring Boot application. It is time to learn how to implement one!

    Prerequisites

    First, you need a Spring Boot Web application in Java. You can create one with Spring Initializr as follows:

    You are free to choose Gradle or Maven as the build tool, but you must select Java as the programming language and add Spring Web as a dependency. This way, Spring Initializer will automatically add spring-boot-starter-web to your project’s dependencies. Note that Spring Web includes RestTemplate– the HTTP client used by the Spring Boot API Layer to perform HTTP requests.

    Implementing an API Layer

    Let’s see how you can add an API layer to a Spring Boot Web app. The first thing you need to do is create a utility class to make calling APIs easier.

    In the utils package, you can define an APIHandler.java class as shown below:

    package com.apilayer.service.api.utils;
    
    import org.springframework.boot.web.client.RestTemplateBuilder;
    import org.springframework.http.HttpEntity;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.HttpMethod;
    import org.springframework.http.ResponseEntity;
    import org.springframework.stereotype.Component;
    import org.springframework.web.client.RestClientException;
    import org.springframework.web.client.RestTemplate;
    
    @Component
    public class APIHandler {
        private final RestTemplate restTemplate;
        public APIHandler(RestTemplateBuilder restTemplateBuilder) {
            // initializing a RestTemplate instance
            // to perform API calls
            this.restTemplate = restTemplateBuilder.build();
        }
    
        // defining a utility function
        // to perform API calls
        public <T, R> ResponseEntity<R> callAPI(
                String apiEndpoint,
                HttpMethod httpMethod,
                HttpHeaders httpHeaders,
                T requestBody,
                Class<R> classToConvertBodyTo
        ) {
            // setting the HTTP headers and HTTP body (if present)
            HttpEntity<T> requestEntity = new HttpEntity<>(requestBody, httpHeaders);
    
            try {
                // performing the HTTP request
                return restTemplate.exchange(
                        apiEndpoint,
                        httpMethod,
                        requestEntity,
                        classToConvertBodyTo
                );
            } catch (RestClientException e) {
                // logging errors when the HTTP request fails
                // or there is an error in decoding the response
                e.printStackTrace();
    
                // other default behavior...
                // (e.g. register the error in an APM service, ...)
    
                throw e;
            }
        }
    }

    As you can see, this class initializes an instance of RestTemplate in the constructor with RestTemplateBuilder. Then, it uses that instance in callAPI() to make HTTP requests via the RestTemplate exchange() method.

    The advantage of centralizing API call logic in an APIHandler utility class is that you can define custom behavior for all your calls directly here. For example, you can log errors in callAPI() when the exchange() method raises a RestClientException. This is particularly useful for debugging purposes.

    Similarly, you can add custom global configs to the RestTemplate instance when initializing it in the constructor. For example, you can add a custom HTTP header to the request performed with that RestTemplate instance using the defaultHeader()method:

    // add a custom HTTP header to the RestTemplate instance
    RestTemplate scrapingRestTemplate = new RestTemplateBuilder()
                .defaultHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36")
                .build();

    Therefore, as different third-party services typically have different requirements in terms of headers or procedures for calling their APIs, you may need to create many APIHandlers. Each will have dedicated utility functions that rely on a specifically configured RestTemplate instance.

    Now, let’s see how to use APIHandler to build an <ExternalService>API.java API layer file:

    package com.apilayer.service.api;
    
    import com.apilayer.data.pokeapi.NamedApiResourceList;
    import com.apilayer.service.api.utils.APIHandler;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.HttpMethod;
    import org.springframework.http.ResponseEntity;
    import org.springframework.stereotype.Component;
    import org.springframework.web.util.UriComponentsBuilder;
    import com.apilayer.data.User;
    
    @Component
    public class AuthorizationAPI {
    
        private final APIHandler apiHandler;
    
        private final String BASE_URL = "https://your-auth-backend.com/api/v1/users";
    
        AuthorizationAPI(@Autowired APIHandler apiHandler) {
            this.apiHandler = apiHandler;
        }
    
        public User getUserDataByToken(String token) {
            // retrieving a single User by token
            ResponseEntity<User> response = apiHandler.callAPI(
                    String.format(BASE_URL + "?token=%s", token),
                    HttpMethod.GET,
                    null,
                    null,
                    User.class
            );
            
            return response.getBody();
        }
    
        public bool isAuthenticated(String token) {
            // inserting the token string in the
            // HTTP "Authorization" header
            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.add(HttpHeaders.AUTHORIZATION, token);
    
            // verifying whether a user is authenticated 
            // by checking his authorization token 
            ResponseEntity<Boolean> response = apiHandler.callAPI(
                    BASE_URL,
                    HttpMethod.GET,
                    httpHeaders,
                    null,
                    Boolean.class
            );
            
            return response.getBody();
        }
    
        // other authorization API functions...
    }

    Your backend may rely on different APIs from your authentication backend. Each of these APIs will correspond to an API layer function, which contains all the logic to call the API encapsulated in a few lines of code. Do not forget that this is just one <ExternalService>API.java file, and your Spring Boot app might require many more API layer files.

    Integrating the Retry Logic

    As explained earlier, you now need Spring Retry. Since Spring Retry annotations require the Spring AOP dependency, you need to install two packages. Let’s add both spring-retry and spring-boot-starter-aop to your project’s dependencies.

    If you are a Maven user, add the following lines inside the <dependencies> tag of your pom.xml file:

    <dependency>
        <groupId>org.springframework.retry</groupId>
        <artifactId>spring-retry</artifactId>
        <version>2.0.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
        <version>3.0.2</version>
    </dependency>  

    Otherwise, if you are a Gradle user, add the two lines below in the dependencies object:

    implementation "org.springframework.retry:spring-retry:2.0.0"
    implementation "org.springframework.boot:spring-boot-starter-aop:3.0.2"

    Enable Spring Retry by decorating your @SpringBootApplication class or one of your @Configuration classes with the @EnableRetry annotation:

    You can now use @Retryable to implement retry logic in the APIHandler callAPI() method as shown below:
    package com.apilayer;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.retry.annotation.EnableRetry;
    
    @SpringBootApplication
    @EnableRetry
    public class DemoApplication {
    
       public static void main(String[] args) {
          SpringApplication.run(DemoApplication.class, args);
       }
    
    }

    You can now use @Retryable to implement retry logic in the APIHandler callAPI() method as shown below:

    @Retryable(
            // attempting up to 10 times
            maxAttempts = 10,
            // with a delay of 2 seconds between attempts
            backoff = @Backoff(delay = 2000),
            // when the Http5xxException is raised by the method
            retryFor = Http5xxException.class
    )
    public <T, R> ResponseEntity<R> callAPIWithRetryLogic(
            String apiEndpoint,
            HttpMethod httpMethod,
            HttpHeaders headers,
            T body,
            Class<R> classToConvertBodyTo
    ) {
        // setting the HTTP headers and HTTP body (if present)
        HttpEntity<T> requestEntity = new HttpEntity<>(body, headers);
    
        try {
            // trying to perform the request
            return restTemplate.exchange(
                        apiEndpoint,
                        httpMethod,
                        requestEntity,
                        classToConvertBodyTo
            );
        } catch (RestClientResponseException e) {
            // in case of a 5xx error, throwing a custom
            // exception that triggers the retry logic
            if (e.getStatusCode().is5xxServerError()) {
                throw new Http5xxException(e.getMessage());
            }
           
            // some default behavior...
            e.printStackTrace();
            
            throw e;
        } catch (RestClientException e) {
            // logging errors when the HTTP request fails
            // or there is an error in decoding the response
            e.printStackTrace();
    
    
            // other default behavior...
            // (e.g. register the error in an APM service, ...)
    
            throw e;
        }
    }

    Now, when the server returns a 5xx HTTP error, the backend will try up to ten times to execute the API call. Note that retrying on 4xx HTTP errors does not make sense. This is because 4xx errors are client errors, and retrying would result in the same error over and over again.

    In detail, callAPIWithRetryLogic() retries on Http5xxExceptions. This is a custom error exception that you can define as follows:

    package com.apilayer.exceptions;
    
    public class Http5xxException extends RuntimeException {
        public Http5xxException(String message) {
            super(message);
        }
    }

    Note that not all API calls need repeat logic. For this reason, you should define two callAPI()-like functions in APIHandler and annotate only one with @Retryable. You can name these two functions callAPIOnce() and callAPIWithRetry(). To make APIHandler more elegant and avoid repeating code, you should define a private function callAPI() containing the logic to call an API and make the two functions use it as done in the demo codebase.

    Furthermore, you should define two callAPI()-like functions in APIHandler and annotate only one with @Retryable.

    Well done! You just learned how to implement an API layer in Spring Boot with RestTemplate and Spring Retry!

    API Layer in Action

    Let’s now see the API layer in action in a Spring Boot demo application. As a third-party sample service, the application uses the free-to-use PokeAPI service. Also, it relies on the Java API models from the pokeapi-reactor repo.

    Clone the GitHub repository supporting this article and launch the demo app locally by running the following commands:

    git clone https://github.com/Tonel/api-layer-spring-boot
    cd api-layer-spring-boot
    ./gradlew build
    ./gradlew bootRun

    You can now call the /v1/getPokemons endpoint with the curl command below:

    curl -H "Accept: application/json" -H "Content-Type: application/json" -X GET "http://localhost:8080/api/v1/pokemon/getBlastoise"

    Alternatively, visit http://localhost:8080/api/v1/pokemon/getBlastoise in your browser.

    In both cases, you should get the following result:

    {
      "id": 9,
      "name": "blastoise",
      "image": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/9.png",
      "height": "160 cm",
      "weight": "85 kg"
    }

    This is nothing more than a transformed version of some data retrieved from a PokeAPI API call!

    Conclusion

    In this tutorial, you learned what a backend API layer is, its essential benefits in the microservices world, and how to integrate it into a Spring Boot Web application. The API layer is nothing more than a set of components that provide complete functionality for sending and receiving data via API calls. This allows you to consolidate API logic into a single layer of the Spring Boot application architecture, making the entire codebase easier to maintain and more reliable, reducing the possibility for human error . As demonstrated in a complete example, implementing an API layer in Spring Boot is simple and takes only a few minutes.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    mm
    Writen by:
    I'm a software engineer, but I prefer to call myself a Technology Bishop. Spreading knowledge through writing is my mission.
    Avatar for Antonello Zanini
    Reviewed by:
    I picked up most of my soft/hardware troubleshooting skills in the US Army. A decade of Java development drove me to operations, scaling infrastructure to cope with the thundering herd. Engineering coach and CTO of Teleclinic.