Backend For Frontend (BFF)

BFF is a pattern where you create a dedicated backend for each frontend where you can optimize the API for the specific needs of the frontend.

September 28, 2024 architecture

Introduction

In a tipical development, the frontend application consumes data from a backend API. The backend API is designed to be generic and serve multiple clients which can lead to a situation where the frontend application needs to make multiple requests to the backend API to get all the data it needs. This can lead to performance issues and a poor user experience. Additional, the data returned by the backend API may contain more data than the frontend application needs or data that is not in the format that the frontend application expects.

A flow with gateway

In the image above, the frontend application needs to make multiple requests to the backend API to get all the data it needs.

Backend For Frontend

To solve these issues, you can create a dedicated backend which is optimized for the specific needs of the frontend application. This dedicated backend is called Backend For Frontend (BFF). In this backend you can extend the API with new properties, reduce the amount of data or join data from multiple sources to reduce the number of requests from the frontend application.

Example with a single service

Let’s say you want to add to a frontend application a list that displays cron jobs. The backend API is already created and returns a list of cron jobs with the following properties:

GET /crons
[
{
"id": 1,
"name": "Backup",
"status": "running",
"cronExpression": "0 0 * * *"
},
{
"id": 2,
"name": "Cleanup",
"status": "stopped",
"cronExpression": "0 1 * * *"
}
]

But bussiness requirements says that the frontend application should also display the next execution time of each cron job and a human readable description of the cron schedule.

Without BFF

The frontend application needs resolve a next execution time and a human readable description of the cron schedule by itself. Parsing cron expression is not a trivial task and moreover the description should be translated to a user language. At the end you will end with a library to parse cron expressions and another library to translate cron expression to the human readable form. This will increase the bundle size and the complexity of the frontend application.

frontend.js
import cronParser from "cron-parser";
import cronstrue from "cronstrue";
// somewhere in the frontend application
function getCronDetails(cronExpression) {
const nextExecutionTime = cronParser.parseExpression(cronExpression).next().toString();
const humanReadableCronExpression = cronstrue.toString(cronExpression);
return { nextExecutionTime, humanReadableCronExpression };
}

A flow between FE and Cron API

With BFF

With BFF you can extend the API with the next execution time and the human readable name of the cron expression and return excatly what the frontend application needs. Of course you still need to handle calculations and translations but at least you can do it in the backend without increasing the bundle size and the complexity of the frontend application.

A flow between FE, BFF and Cron API

express.js
import express from "express";
import cronParser from "cron-parser";
import cronstrue from "cronstrue";
const app = express();
// BFF logic
app.get("/crons", async (req, res) => {
const cronList = await fetch("https://cron-api.example.com/crons");
const cronJobsWithExtraProperties = cronList.map((cronJob) => {
const interval = cronParser.parseExpression(cronJob.cronExpression);
const nextExecutionTime = interval.next().toString();
const humanReadableCronExpression = cronstrue.toString(cronJob.cronExpression);
delete cronJob.cronExpression;
return {
...cronJob,
nextExecutionTime,
humanReadableCronExpression,
};
});
res.json(cronJobsWithExtraProperties);
});
express.js
import express from "express";
import cronParser from "cron-parser";
import cronstrue from "cronstrue";
const app = express();
// BFF logic
app.get("/crons", async (req, res) => {
const [cronList, translations] = await Promise.all([
fetch("https://cron-api.example.com/crons"),
fetch("https://translations-service/translations"),
]);
const cronJobsWithExtraProperties = cronList.map((cronJob) => {
const interval = cronParser.parseExpression(cronJob.cronExpression);
const nextExecutionTime = interval.next().toString();
const humanReadableCronExpression = cronstrue.toString(cronJob.cronExpression);
return {
...cronJob,
status: translations[cronJob.status],
nextExecutionTime,
humanReadableCronExpression,
};
});
res.json(cronJobsWithExtraProperties);
});

Example with multiple services

Let’s say you want to create a list of products in a frontend application. We have many services in the architecture that provide data about products. The first service provides data about the stock of the products, the second service provides static data about the products and the third service provides translations.

A flow between FE and multiple services

As you can see to get all the data needed to display a list of products in the frontend application you need to make multiple requests to multiple services. Of course you can do it in the frontend application but it will increase the number of requests and perhaps the frontend application will need to handle the translations and join the data from multiple sources. You can resolve those issues by creating a decidated endpoint just for displaying a list of products in BFF service.

product-list.js
import express from "express";
const app = express();
app.get("/products-list", async (req, res) => {
const ids = req.query.ids.split(",");
const language = req.query.language;
const [stockData, staticData, translations] = await Promise.all([
fetch(`https://products-api/products?ids=${ids.join(",")}`),
fetch(`https://products-static-data-api/products-static-data?ids=${ids.join(",")}&language=${language}`),
fetch("https://translations-service/translations"),
]);
const productsList = stockData.map((product) => {
const productStaticData = staticData.find((el) => el.id === product.id);
return {
id: product.id,
price: product.price,
stock: product.stock > 0 ? translations["In Stock"] : translations["Out of Stock"],
title: productStaticData.title,
mainImage: productStaticData.images[0],
};
});
res.json(productsList);
});

Pros

Cons

Q&A

Which language should I use to create a BFF?

You can use any language that you are comfortable with but if I would recommend using the same language as the FE application. Thanks to that you can share the code (and libraries!) between the FE and BFF.

Should I create a separate repository for the BFF?

Not really. Having a mono-repo with the FE and BFF can simplify the development process. When you need to make changes in the FE and BFF at the same time you can do it in a single pull request (it is especially useful when you need to break a contract between the FE and BFF). It is also easier to keep the versions of the FE and BFF in sync.

About about GraphQL?

GraphQL is a alternative to BFF. When you use GraphQL then you should not need a BFF.

Do you like the content?

Your support helps me continue my work. Please consider making a donation.

Donations are accepted through PayPal or Stripe. You do not need a account to donate. All major credit cards are accepted.

Leave a comment