DEV Community

Cover image for BFF - Backend for Frontend Design Pattern with Next.js
Adel
Adel

Posted on • Updated on

BFF - Backend for Frontend Design Pattern with Next.js

Intro

These days microservice architecture becoming more and more popular, and if you worked on a project that adopt this architecture then as a frontend developer you probably faced one of the following scenarios:

  • You support multiple platforms ( web, mobile app, smartwatch…. ) and each one has a specific need of data.
  • Calling APIs from multiple services to build one user interface.
  • Manipulate, mix and match the responses of multiple API calls to reach the desired shape of data.
  • Receive unnecessary data from API that you don't need at all.
  • Receiving the same piece of info from different services with the different data types, for example one service could send the date as epoch and other one might send it as a Linux timestamp.
  • Finding yourself writing complex computations or maybe business logic in the frontend.

As your codebase grows and becomes more complex, it becomes hard to keep organized, and by the time you might find your codebase out of control and of course complexity where the bugs hide.

Typically the frontend code should be very simple, straight forward and easy to read, and we should avoid doing complex computations in the UI layer especially while rendering, otherwise you will be using a lot more of browser resources which will lead to bad performance .

General Purpose API

Generic APIs contain unnecessary data that is sometimes of no use for consumer applications This might be critical sometimes especially when sometimes we need to deliver as small response as possible to some frontends like smartwatches.

Each one of these frontends may have specific needs about the data delivered to it from the backend. And since all of them calling the same API, the backend developer will try to spit every piece of data available to satisfy all frontends needs.

What Is BFF Design Pattern

This pattern was first described by Sam Newman.

By Implementing BFF we try to keep the frontend decoupled from the backend. The BFF should be tightly coupled with frontend, Because in the first place it existed to serve the frontend needs and Ideally it should be built by the frontend developer.

In most cases we should have one BFF for each frontend, then we can customize the BFF and fine-tune it according to that frontend needs.

In some cases we might share one BFF with multiple frontends if the requirements are very similar, for example one BFF for iOS and Android this way is adopted by SoundCloud for example, by doing this you will avoid a lot of duplicate code across BFFs.

One BFF per frontend One BFF per frontend

Sharing BFF for some frontends Sharing BFF for some frontends

Not an API gateway: you might think that the BFF is very similar to API gateway but it's not because the main reason for API gateway is to be a reverse proxy between the consumer and all other microservices not to customize the response according to this particular frontend needs. Also API gateway is the single entry point for anybody need to reach any backend service wheather the BFF is specific to one frontend.

BFF will hide a lot of complexities from the frontend which will make the app more resilient to new change.
Also you have the freedom the use any protocol you are most comfortable with like GraphQL, even if the other services use REST or SOAP.

Using BFF will also abstract the frontend related unit tests .

Note that the BFF pattern is not useful when you only support one frontend.

With Multiple Backend services

Lets say you need to build a user profile page for a social platform, and this platform is built with microservices architecture, then it will look something like this.

As you see here the web UI calls APIs from multiple services to build the profile page.
First needs to get the data about the user, and do another two or more calls to get the rest of the results based on the fetched username or user id. Note that the response could contain a lot of data that is not needed to build this UI, the latter calls can be called on parallel to be executed in less time, then you need to merge the responses and gather only the data you need to build this user profile page. It looks painful right? Imagine you have similar scenarios with much more complex UIs and much more services to consume data from, this is not very practical.

Instead it will be more efficient to call just one API and get only the data needed to build this page, and this is what needs to happen in the BFF layer.

In this way we abstracted all this complexity from the frontend, and the frontend role here is just to present the returned data.

I will do an example for the same problem later in this article.

API Versioning and A/B testing

Sometimes you maybe supporting different versions of the API for the same service, it's much easier to abstract this from the frontend and do it inside the BFF. This way the frontend will not be aware of the version it will just render the UI no matter what.

It can be useful as well when you want to run A/B testing campaign, for example you can return the version needed for specific users with the user object then let the BFF handle different API versions.

Nice Additions, Taking it further

Now after you added the BFF layer, there is a lot of cool things you can do specifically to that frontend.

  • Security: Because you are sending only what the frontend needs, you are hiding a lot of unnecessary or sensitive data that the attacker might use against you.
  • Caching: You can connect to redis for example directly and cache the API responses, then serve the results from the cache if available instead of calling the microservice.
  • Error Handling: multiple services can handle errors in different ways, in the BFF you can define a unified way to give the frontend a consistent response in case of any error happens.
  • Access Control
  • Logging
  • Web Sockets
  • etc …

Although I think it's better to keep it as simple as you can and stick to the main reason of building this BFF which is solving the problems of that specific frontend not solving general problems.

As the codebase grows, you might find yourself implementing general-purpose tiny services inside the BFF (sound cloud faced this issue) so try to keep the scope of the BFF as it's defined from the beginning.

With Next.js

By using Next.js you will get a few benefits out of the box

  • Fewer deployments: you don’t need to deploy your BFF separately because it will be integrated with Next.js by default.
  • Using the backend layer in Next.js, BFF will be tightly coupled to your frontend which is what we exactly need.
  • Sharing code like type definitions and utility functions between BFF and the frontend will be very easy.

For the sake of demonstrating how BFF work we will use Next.js API to simulate the microservices behavior, so we will have one file for each of the following:

  • Messaging service will include
    • One endpoint to get all messages based on "read" filter, and it can take two values (true, false).
    • One endpoint to get the latest message received (to get the last seen).
  • Notification service will include one endpoint to get all notifications based on "seen" filter and it can take two values (1,0).
  • Friends service will include one endpoint to get all pending friend requests.
  • BFF itself will consume APIs from all of those services.

First, we will see how the data will look like from each service.

Message object
    {
        "uid": "263f4178-39c6-4b41-ad5b-962a94682ceb",
        "text": "Morbi odio odio, elementum eu, interdum eu, tincidunt in, leo. Maecenas pulvinar lobortis est. Phasellus sit amet erat. Nulla tempus.",
        "created_at": "1634320826",
        "read": false
    }
Enter fullscreen mode Exit fullscreen mode
Notification object
    {
        "uid": "ee7cd9df-2409-46af-9016-83a1b951f2fa",
        "text": "Vestibulum quam sapien, varius ut, blandit non, interdum in, ante.",
        "created_at": "1617738727000",
        "seen": 0
    }
Enter fullscreen mode Exit fullscreen mode
Person object
    {
        "id": 1,
        "first_name": "Marillin",
        "last_name": "Pollicott",
        "birthdate": "4/20/2021",
        "email": "mpollicott0@wikispaces.com",
        "gender": "Male",
        "ip_address": "105.134.26.93",
        "address": "2132 Leroy Park",
        "created_at": "9/13/2021"
    }
Enter fullscreen mode Exit fullscreen mode
Desired profile object
{
    "name": "John Doe",
    "birthdate": "2020-11-17T00:00:00.000Z",
    "address": "242 Vermont Parkway",
    "joined": "2021-08-27T00:00:00.000Z",
    "last_seen": "2021-10-15T18:00:26.000Z",
    "new_notifications": 61,
    "new_messages": 56,
    "new_friend_requests": 15
}
Enter fullscreen mode Exit fullscreen mode

Notice the differences in data types for each service, like date, in message object it's a Linux timestamp in seconds and in notification service it's Linux timestamp in milliseconds while it's just simple date string in friends service and what we want actually is a simplified extended ISO format with the timezone set to zero UTC offset so it can be formatted in the frontend however we want. You can see also message service the Boolean represented as (true, false) and in notification service it's (1,0) you can spot other differences too if you look in details.

Also, notice the person object we have first and last name as different attributes, but in the frontend we show the combination of both.

So the main task of the BFF is to get data from different services, gather them and format them in the easiest form so the frontend will do the least effort to render these data. For that we defined a new interface (Profile).

interface Profile {
   name: string
   birthdate: Date
   address: string
   joined: Date
   last_seen: Date
   new_notifications: number
   new_messages: number
   new_friend_requests: number
}
Enter fullscreen mode Exit fullscreen mode

In this interface we described the data we want and in which type to guarantee that the response returned to the frontend will be always correct.

You can check the code on this link
The demo on this link

One more cool thing with Next.js
If you are planning to integrate with some sort of caching mechanism like redis, next.js will make it much more easy and performant.

With server-side rendering in next.js you can just get the data from redis and just send the page ready to the frontend without the need to call an API from the frontend, the data will just be there in the fastest way possible.

TL;DR

  • BFF focuses on Creating a new backend per frontend that only serves the needs of that frontend.
  • BFF will call APIs from multiple services and form the minimal response required.
  • Frontend will get only what is needed to render the UI.

Further Reads

https://samnewman.io/patterns/architectural/bff
https://developers.soundcloud.com/blog/service-architecture-1
https://docs.microsoft.com/en-us/azure/architecture/patterns/backends-for-frontends

Discussion (0)