Make frontend to microservice communication great again

Make frontend to microservice communication great again
Photo by Pavan Trikutam / Unsplash

Your frontend is still stuck on a monolithic architecture, while your backend seems to get increasingly distributed? Do you see yourself doing too many API calls in your frontend, maybe even worsening it with different structures? Then this is for you.

Abstract it away

The first and easiest way is to put all API calls into services. If you are using a modern framework, chances are high that you already have dependency injection in place, so move network calls directly out of your logic into services.

This is a pretty obvious one, I assume, but wanted to list it as a starting point anyway.

Instead of calling the service directly in your interaction, e.g., like this:

async function clickHandler(event) {
	const data = await fetch("https://my.api/relevantData")
    .then(r => r.json());
    
   	setData(data);
}

Create a service tailored to your needs:

class ApiService {
    constructor(baseUrl) {
    	this.baseUrl = baseUrl;
    }
    
	async _fetchData(path) {
    	return fetch(`${this.baseUrl}${path}`)
        	.then(r => r.json());
    }
    
    async getRelevantData() {
    	return this._fetchData("/relevantData")
    }
}
    
async function clickHandler(event) {
    // assuming context gives us each service needed by a registered identifier
	const data = await context.getService("api").getRelevanData();
    setData(data);
}

This way you abstract way whole handling. Of course in a real code there would be error handling, authentication etc.

Build an API-Gateway

An API-Gateway consolidates the calls to backend services to have a common entry point. In some cases, people even offload authentication and authorization to the gateway, making microservices focus only on the actual tasks.

Calls are now at least for a single host, possibly even sharing the same authentication, reducing complexity on the frontend side. As this is a rather architectural shift, this requires a bit more effort and might involve a lot more parties.

So assuming the following calls before:

GET orders-service.company.services/v1/orders
GET orders-service.company.services/v1/orders/1
PUT stock-service.company.services/v2/stock/2345

becomes:

GET api.company.services/orders/v1/orders
GET api.company.services/orders/v1/orders/1
PUT api.company.services/stock/v2/stock/2345

This also leads to better connection reuse, as the same domain is used and the connection can be reused by the browser.

It also reduces complexity for abstracting the service layer.

Create small backend layer

For use cases where building an API-Gateway for the entire company or a scoped architectural domain is not sufficient or not possible at all, this one comes handy.

The idea is pretty simple: You create an as minimal as possible layer between your frontend and backend.

Use Case 1: Simplify/Improve API responses

So let's assume your API usually returns a response like this:

{
   "data":{
      "response":{
         "items":[
            {
               "name":"cat"
            },
            {
               "name":"dog"
            },
            {
               "name":"fish"
            }
         ],
         "ok":true
      }
   },
   "statusCode":200
}

That's a lot of unnecessary data, while this might not matter for your code. Since you can just put it into an abstraction layer, directly in the frontend, you still need to transmit the data to the user through the internet. It is also really cumbersome to use, as the most data is duplicated from HTTP.

Your abstraction layer can transform the data to look cleaner and easier to use:

[
    {
        "name":"cat"
    },
    {
        "name":"dog"
    },
    {
        "name":"fish"
    }
]

When ok == false, then you can just return a 500 instead with a proper message, now your frontend code will be much cleaner, using fewer data.

Use Case 2: Combine requests

Every so often you need to do a request just as an intermediate step to get more data.

Let's assume an endpoint that gives you only the IDs of the last five orders for a customer, with that IDs you can do these calls to the orders API.

Let's assume the given response:

[
    "ORDER-000001",
    "ORDER-000010",
    "ORDER-234525",
    "ORDER-230546",
    "ORDER-304602"
]

This leads to these requests:

GET orders.company.services/ORDER-00001
GET orders.company.services/ORDER-000010
GET orders.company.services/ORDER-234525
GET orders.company.services/ORDER-230546
GET orders.company.services/ORDER-304602

Just to display the information in the frontend. Much cleaner would be to have a single call for the last five orders, giving you a single response.

While this is not only convenient, it also reduces wait time and reduces load from the client.

So, it would like this from the frontend:

GET orders-page.company.services/latest-orders

While this might look a bit made up at first, you will definitely find so many places where you have combinations like these, that might also be not obvious at first.

Use Case 3: Avoid CORS problems

Since not all API endpoints are using the same domain as your frontend, there will come a point where you need to have a request to another domain. So, Cross-Origin Resource Sharing takes place. This implies that your browser receives according security headers and most importantly can run an OPTIONS request before doing the actual one to verify everything is fine.

Usually, you are not the owner of each API or simply can't wait: So a semi-transparent proxy is the way to go. This means you route the requests to the origin as they are and just add the few required CORS header to the response. In addition, you implement a routing for OPTIONS requests to just return a 2xx status code with the correct security headers.

Use Case 4: Caching

When your API is slow as hell and returning cacheable data, this additional layer enables you to do some server-side caching with a breeze. Add Redis, Memcached, EHCache, you name it! It is really as simple as that.

A word of caution

But don't get confused, sometimes these side effects are intended, e.g., to avoid blocking for a long time. So if you need the orders' response for initial display, it is fine to have them in parallel. At the end of the day, it strongly depends on your use case.

The reality is that good APIs are not built for a specific use case, but more general and following general concepts. Simply, this doesn't align all the time with the specific need for your frontend app.

Be an advocate to your backend devs

Many backend devs, no matter the seniority, who never had to use an API from the frontend have no clue what problems you experience. On the server-side, most of these calls are a different kind of story.

Giving them awareness and feedback on API endpoints is a vital tool for improving quality and understanding for new features, leading to better overall experience for both sides.

Conclusion

With this information in mind, you can hopefully make your frontend less cluttered and improve the experience for developers and users.

Do you utilize something else as well, or have any tips? You are welcome to add them to the comments.