11 Nov 2022 Β· Software Engineering

    Why Your Backend in Node.JS Needs an API Layer and How to Build It

    12 min read
    Contents

    In the microservice architecture, each service is independent and communicates with other services via API. Considering how popular the microservices approach is, your backend probably needs to call external APIs.

    Calling an API in your backend is simple and requires only a few lines of code. At the same time, your backend may contain the same logic to make an API call in several places. This leads to code duplication and makes your codebase less maintainable. With this approach, when the endpoint of an API changes, you are forced to update the code at all points where it is called. This is tedious and vulnerable to human error.

    Now, suppose you add a layer to your architecture that contains everything you need to call all the APIs that your backend depends on. Encapsulating all the logic to make API calls in the same place brings several benefits and allows you to avoid the drawbacks mentioned above. This is the main purpose of an API layer.

    Let’s learn what an API layer is, why your backend needs one, and how to equip your Node.js backend with an API layer. Follow this tutorial to learn how to build the result below:

    https://codesandbox.io/s/node-js-api-layer-demo-ihqmp8

    What is an API layer?

    An API layer is the part of the backend application that contains the programming logic to send and receive data via an Interface (API). Thus, all the calls to external APIs that a backend application makes pass through this layer. This concept is not new, and you can also use an API Layer in a frontend architecture. If that interests you, you can also learn more on how to build an API Layer in React.

    All the logic required to build this architectural layer is generally stored in the apis folder. In a Node.js project, you can organize the files that your API layer consists of with the following naming convention:

    <externalService>API.js

    The <externalService> qualifier will make finding APIs of the same type easier. This is because each file in the API layer folder should group all APIs related to the same external microservice. Note that a backend API layer typically also involves some utility and configuration files. Such files do not follow the naming convention mentioned above.

    Here is what an API layer in a Node.js application may look like:

    apis
    β”œβ”€β”€ configs
    β”‚    β”œβ”€β”€ axiosClients.js
    β”‚    └── axiosUtils.js
    β”‚
    β”œβ”€β”€ AuthorizationAPI.js
    .
    .
    .
    β”œβ”€β”€ CmsAPI.js
    .
    .
    .
    └── SemaphoreAPI.js

    As you can see, you can group all the APIs of the external services that your backend application relies on in the API layer.

    Then, you can call an API in a controller as follows:

    // src/controllers/articles.js
    const { CmsAPI } = require("../apis/cmsAPI")
    
    const CmsController = {
      getArticles: async (req, res) => {
        const articles = await CmsAPI.getAllArticles()
    
        // performing other operations...
    
        res.json(articles)
      },
    }
    
    module.exports = { CmsController }

    This single example shows that the functions exposed by the API layer offer everything you need to send or receive data via API. In other words, these functions encapsulate all the logic for calling APIs. At the same time, calling an API is an asynchronous operation. This means that these functions do not actually perform the API call, rather they return a Promise. If you are not familiar with this, a promise is a proxy for an unknown value that will be returned in the future asynchronously.

    The Promise technology allows you to separate the place where API calls are defined from the place where they are actually performed. This is what an API layer is all about. An API layer, therefore, requires a Promise-based HTTP client to be implemented, such as axios.

    In detail, the async functions defined in the API layer use the HTTP client to implement the logic for calling an API and return a Promise. So, you really only get the API response when you wait for it with an await operator. This is when the API gets called. Specifically, the await operator allows you to wait for a Promise coming from an async function and get its fulfillment value synchronously. You can learn more about how to use async and await with Promise here.

    Let’s now look at the advantages that an API layer can provide to your backend architecture.

    Why your backend architecture should have an API layer

    There are several benefits that an API layer can bring to your backend architecture. Let’s focus on the three most important reasons why you should adopt it.

    1. An API layer simplifies integration of third-party services 

    Most SaaS (Software as a Service) solutions, e.g. Semaphore, are now API-based. This means that they provide access to their features and data via API. Since you do not want to reinvent the wheel, you should rely on the best systems on the market to build your backend application. This is especially true if your technology stack is based on a composable architecture. We are going to use Semaphore for the purposes of this article, but this approach can be applied with other SaaS solutions as well.

    Thanks to the API layer, you already have infrastructure to get started. If you want to integrate a new third-party service into your backend, you only have to create a specific axios instance as follows:

    // src/apis/configs/axiosClients.js
    const axios = require("axios")
    
    // initializing the axios instance for the Semaphore APIs
    // with custom configs
    const sempahoreClient = axios.create({
      baseURL: "https://<YOUR_ORG_NAME>.semaphoreci.com/api/v1alpha",   
      headers: {
        "Authorization:": `Token <YOUR_API_TOKEN>`,
      },
    });
    
    module.exports = { sempahoreClient }

    Then, you can use this instance to define the Semaphore API calls in your SemaphoreAPI.js file. Note that the configs/axiosClients.js file should contain each axios client associated with a <externalService>API.js file.

    With an API layer, integrating external services takes only a few lines of code and is all centralized in the same place. This also means that switching to a different service that provides similar features does not require major refactoring. You would only have to change the corresponding <externalService>API.js file.

    2. An API layer increases the quality of your codebase

    In the microservice architecture, your backend application relies on APIs to communicate with other services. This means that your backend is likely to perform multiple API calls. Also, your application is likely to call the same API in different places. With a traditional architecture, this means repeating the following logic required to call the API each time:

      // the logic required to call an API 
        // with the axios HTTP client
        const response = await axios.request({
            method: "GET",
            url: `https://your-cms.com/api/articles`
        })
        
        const articles = response.data.articles

    In this example, you have to write these lines of code each time you want to retrieve the list of all articles from your CMS via API. If you need to call this API in different places, this approach involves code duplication.

    Now, let’s assume that you have centralized all the logic needed to call APIs in the API layer. If an API endpoint changes, you only need to change one function. Without the layer, you would have to update each API call made throughout the codebase.

    Therefore, an API layer makes your backend application easier to maintain and cleaner, without the same logic repeated over and over again. This also reduces the potential for human error.

    3. An API layer makes it easier to deal with retry logic

    Thanks to the centralized nature of the API layer, you can effortlessly add custom features to all API calls belonging to the same service. If you know that one service which your backend relies on is likely to respond with server-side 5xx HTTP errors, you should implement retry logic. This is particularly true when dealing with 503 Service Unavailable errors.

    Implementing retry logic in all of a service’s API calls without the API layer would mean code duplication. Thanks to the API layer, you can simply tweak your axios client in the axiosClients file as follows:

    axiosRetry(sempahoreClient, {
      retries: 5, // number of retries
      retryDelay: (retryCount) => {
        // waiting time in milliseconds between each retry
        return retryCount * 2000
      },
      retryCondition: (error) => {
        // retrying only on 503 HTTP errors
        return error.response.status === 503
      },
    })

    Here, the axios-retry utility is used to implement the centralized retry logic in just a few lines of code. Now, all Semaphore APIs will be called up to 3 times.

    Note that you should only use retry logic with idempotent APIs. If you are not familiar with this, idempotency is the property of a certain operation which means that it can be executed multiple times without changing the result.

    Implementing an API layer in Node.js with axios

    Now that you know what an API layer is and why it is so important in a backend application, let’s implement one in Node.js.

    Prerequisites

    As stated earlier, an API layer is based on a Promise-based HTTP client. Here you will see how to build an API layer with axios, one of the most used Promise-based JavaScript HTTP clients. Do not forget that any other Promise-based HTTP client will do, so you could also opt for the node-fetch npm library.

    Add axios to your project’s dependencies as follows:

    npm install axios

    You will also need the axios-retry plugin. You can install it with the command below:

    npm install axios-retry

    Adding the API layer with retry logic

    Let’s now learn how to add an API layer to your Node.js backend application. First, let’s define the configuration files.

    This is what a configs/axiosClients.js may look like:

    // src/apis/configs/axiosClients.js
    const axios = require("axios")
    const axiosRetry = require("axios-retry")
    const { errorHandler } = require("./axiosUtils")
    
    // defining the axios client for the headless CMS service
    const cmsClient = axios.create({
      baseURL: "https://your-cms.com/api/v1",
    })
    
    // registering the custom error handler to the
    // cmsClient axios instance
    cmsClient.interceptors.response.use(undefined, (error) => {
      return errorHandler(error)
    })
    
    axiosRetry(cmsClient, {
      retries: 3, // number of retries
      retryDelay: (retryCount) => {
        console.log(`Retry attempt: ${retryCount}`)
    
        // waiting 2 seconds between each retry
        return 2000
      },
      retryCondition: (error) => {
        // retrying only on 503 HTTP errors
        return error.response.status === 503
      },
    })
    
    module.exports = { cmsClient }

    Here you learned how to define a custom axios client for CMS APIs with retry logic and register a custom error handler function with axios interceptors. This is just an example, and you can define the kind of custom behavior that each axios client should have in configs/axiosClients.js .

    Now, let’s see what the errorHandler() function from the configs/axiosUtils.js file looks like:

    // src/apis/configs/axiosUtils.js
    
    // defining a custom error handler for all APIs
    const errorHandler = (error) => {
    const statusCode = error.response?.status
    
    // logging only errors that are not 401
    if (statusCode && statusCode !== 401) {
      console.error(error)
    }
    
    return Promise.reject(error)
    }
    
    module.exports = { errorHandler }

    Again, this is just a simple example. You can enter any utility functions that your axios clients or <externalService>API.js files require.

    Let’s now see how to define an <externalService>API.js API layer file with the cmsClient defined above:

    // src/apis/cmsAPI.js
    const { cmsClient } = require("./configs/axiosClients")
    
    const CmsAPI = {
      get: async function (slug) {
        const response = await cmsClient.request({
          url: `/article/${slug}`,
          method: "GET",
        })
    
        return response.data
      },
    
      getArticles: async function (limit, offset) {
        const response = await cmsClient.request({
          url: "/articles",
          method: "GET",
          params: {
            limit: limit,
            offset: offset,
          },
        })
    
        return response.data.entities
      },
    
      getAllArticles: async function () {
        const response = await cmsClient.request({
          url: "/articles",
          method: "GET",
        })
    
        return response.data.entities
      },
    
      // other CMS APIs required by the backend application ...
    }
    
    module.exports = { CmsAPI }

    As you can see, each function that the API layer consists of is very simple. Keep in mind that there is just one <externalService>API.js file, and your backend might need many more API layer files.

    API layer in action

    Let’s now take a look at the API layer in action in a Node.js demo application that relies on the free and open-to-use PokeAPI project as a third-party sample service.

    You can clone the GitHub repository that supports this article and try it locally by launching the commands below:

    git clone https://github.com/Tonel/api-layer-nodejs-demo-semaphore
    cd api-layer-nodejs-demo-semaphore
    npm install
    npm run start

    Then, you call the /v1/getPokemons API with the command below:

    curl -H "Accept: application/json" -H "Content-Type: application/json" -X GET "http://localhost:5000/api/v1/getCharizard"

    Or by visiting localhost:5000/api/v1/getCharizard in the browser.

    In both cases, you will get the following result:

    {
      "id": 6,
      "name": "charizard",
      "image": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/6.png",
      "height": "170 cm",
      "weight": "90.5 kg"
    }

    This corresponds to a filtered and transformed result from a PokeAPI endpoint.

    By visiting the API URL in the live demo at the beginning of this tutorial, you will get the same result!

    Conclusion

    In this article, you learned what a backend API layer is, the main benefits that it can bring to your backend architecture, and how to implement it in Node.js. To recap, an API layer is a set of files in your architecture that offers everything your backend needs to send and receive data via API calls. It centralizes the API logic and allows you to make your codebase clearer and avoid code duplication. Adding such a layer to a JavaScript backend application is easy. You only need a Promise-based HTTP client.  

    Thanks for reading!

    2 thoughts on “Why Your Backend in Node.JS Needs an API Layer and How to Build It

    1. Thank you for this well-explained article. I especially like the decoupling provided by the API layer.

      If you have the time, can you please help me clarify something: in the section on “Adding the API layer with retry logic”, the first code example creates a cmsClient object, however, the file exports an object with only a pokemonClient property. Is it an oversight or I am the one missing something? Thank you.

      1. I’m the author. Thanks for pointing it out!

        That is a typo.

        The correct line of code should be:
        “module.exports = { cmsClient }”

        The article will be updated soon. Thanks!

    Leave a Reply

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

    Avatar
    Writen by:
    I'm a software engineer, but I prefer to call myself a Technology Bishop. Spreading knowledge through writing is my mission.
    Avatar
    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.