WebApp BFF (Backend-For-Frontend) Concept
Kevin Toshihiro Uehara
Posted on March 25, 2024
Hi there!!! It's nice to have you here again! How have you been? How you doing? Everything all right? I hope so!
In this article I want to introduce you de concept of BFF (Backend-For-Frontend). Let's create an application client and a server to understand, why this concept is so important that you know.
Imagine that your webapp needs some user datas, like posts, albums, todos, user info. Propabaly you will fetch this data using three requests for each service. Then your client is making 4 requests to add the datas and propably there's some informantions and some endpoint that you app don't need it.
Another problem, if you are doing 4 requests your app will need to wait for this 4 Promises wait to finish to render or format the data received.
Or if you have multiples devices apps, a web application, desktop and a mobile app. Probably the data that you will need it's going to be different on each case. Some data in your web applicaton, don't need in your mobile app.
But it major cases, we found the problem of performance. As I mentioned before, imagine that you have 4 ou 5 request to wait and after that continue with data manipulation.
Here is an example:
So let's create this example!
I will use Vite with React and Typescript for our web application.
I will create a directory called, bff-example:
mkdir bff-example
And here I will create both the webapp and the server (simulating the BFF).
First let's create the our webapp using:
yarn create vite webapp --template react-ts
I will enter on project created, install the dependencies (I will use yarn) and run and application
cd webapp
yarn
yarn dev
So here we are, now we have our web application:
Now I will create the types:
src/types.ts
export interface DataResponse {
user: User;
albums: Album[];
todos: Todo[];
posts: Post[];
}
export interface Todo {
userId: number;
id: number;
title: string;
completed: boolean;
}
export interface User {
id: number;
name: string;
username: string;
email: string;
}
export interface Album {
userId: number;
id: number;
title: string;
}
export interface Post {
userId: number;
id: number;
title: string;
body: string;
}
I will use the JSON placeholder to fake our services and API's. you can find: https://jsonplaceholder.typicode.com/
Now I will remove the App.css and index.css. And create the file app.module.css
using CSS Modules.
src/app.module.css
.container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
}
.btn {
padding: 4px;
color: #fff;
background-color: #1d4ed8;
height: 2.8em;
width: 8em;
border-radius: 5px;
cursor: pointer;
}
.btn:hover {
background-color: #3b82f6;
}
.input {
height: 1.8em;
width: 20em;
}
Now I will change the App.tsx to:
src/App.tsx
import { useState } from "react";
import { DataResponse } from "./types";
import style from "./app.module.css";
function App() {
const [data, setData] = useState<DataResponse>();
const [id, setId] = useState<number>();
const getData = async () => {};
return (
<main className={style.container}>
<label htmlFor="userId"></label>
<input
className={style.input}
type="number"
name="userId"
id="userId"
onChange={(e) => setId(+e.target.value)}
/>
<button className={style.btn} onClick={getData}>
Fetch
</button>
<h1>Response:</h1>
{data && <pre>{JSON.stringify(data, null, 4)}</pre>}
</main>
);
}
export default App;
Now we have this screen:
Sorry, I need some styles (LoL). But notice we have an input to insert the user ID and a button to fetch the data when clicked.
Now in getData
function let's call the endpoints:
const BASE_URL = "https://jsonplaceholder.typicode.com";
const getData = async () => {
const userData = await fetch(`${BASE_URL}/users/${id}`);
const user: User = await userData.json();
const postsData = await fetch(`${BASE_URL}/posts?userId=${user.id}`);
const posts: Post[] = await postsData.json();
const todosData = await fetch(`${BASE_URL}/todos?userId=${user.id}`);
const todos: Todo[] = await todosData.json();
const albumsData = await fetch(`${BASE_URL}/albums?userId=${user.id}`);
const albums: Album[] = await albumsData.json();
const formattedResponse = { user, posts, todos, albums };
setData(formattedResponse);
};
Wooww, and now you can see the problem, isn’t? We are making 5 requests on the client side. We need to wait this five requests, to after that, format and render on screen.
BFF Concept
Now let's create a server using NestJS with Node, to simulate our BFF:
On directory bff-example
, let's create our project:
npx @nestjs/cli new bff-server
Now I will create the types, similiar on frontend:
src/types.ts
export interface DataResponse {
user: User;
albums: Album[];
todos: Todo[];
posts: Post[];
}
export interface ErrorDataResponse {
statusCode: number;
status: number;
message: string;
}
export interface Todo {
userId: number;
id: number;
title: string;
completed: boolean;
}
export interface User {
id: number;
name: string;
username: string;
email: string;
}
export interface Album {
userId: number;
id: number;
title: string;
}
export interface Post {
userId: number;
id: number;
title: string;
body: string;
}
I will create the endpoint of GET type and receive the user id as param:
src/app/app.controller.ts
import { Controller, Get, Param } from '@nestjs/common';
import { AppService } from './app.service';
@Controller('data')
export class AppController {
constructor(private readonly appService: AppService) {}
@Get(':id')
getData(@Param('id') id: string) {
return this.appService.getData(id);
}
}
Now on our Service, let's change to call the endpoints that was on client here:
src/app/app.service.ts
import { Injectable } from '@nestjs/common';
import {
Album,
DataResponse,
ErrorDataResponse,
Post,
Todo,
User,
} from '../type';
const BASE_URL = 'https://jsonplaceholder.typicode.com';
@Injectable()
export class AppService {
async getData(id: string): Promise<DataResponse | ErrorDataResponse> {
try {
const userData = await fetch(`${BASE_URL}/users/${id}`);
const user: User = await userData.json();
const postsData = await fetch(`${BASE_URL}/posts?userId=${user.id}`);
const posts: Post[] = await postsData.json();
const todosData = await fetch(`${BASE_URL}/todos?userId=${user.id}`);
const todos: Todo[] = await todosData.json();
const albumsData = await fetch(`${BASE_URL}/albums?userId=${user.id}`);
const albums: Album[] = await albumsData.json();
return { user, posts, todos, albums };
} catch (error) {
return { status: 500, message: 'Error', statusCode: 1000 };
}
}
}
Now let's start our application using:
yarn start
Now if we access the endpoint: http://localhost:3000/data/1
We will receive the same data:
Amazing, isn’t? Now the responsability of the requests it will be made by the server. So now the client will not be overloaded or have performance problems. The BFF and the another services can scale (using Docker and Kubernetes) to respond for our web application.
Now let's go back to our web app and change the getData
function:
const getData = async () => {
// Here we can see an example of BFF
// We can call the four calls here, but it was made on BFF service
// leaving the frontend call only one endpoint
const response = await fetch(`http://localhost:3000/data/${id}`);
const data: DataResponse = await response.json();
setData(data);
};
And let's see the same result:
But now the BFF is responsible to fetch the data and manipulate for our clients (web application, desktop, mobile app).
I want to thank Adriana Saty for giving me the idea to create this article.
Saty Linkedin: https://www.linkedin.com/in/adriana-saty/
And that's it, people!
I hope you liked it.
Thank you so much and stay well, always!!!
Contacts:
Linkedin: https://www.linkedin.com/in/kevin-uehara/
Instagram: https://www.instagram.com/uehara_kevin/
Twitter: https://twitter.com/ueharaDev
Github: https://github.com/kevinuehara
dev.to: https://dev.to/kevin-uehara
Youtube: https://www.youtube.com/@ueharakevin/
Posted on March 25, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.