DEV Community

Cover image for WebApp BFF (Backend-For-Frontend) Concept
Kevin Toshihiro Uehara
Kevin Toshihiro Uehara

Posted on

WebApp BFF (Backend-For-Frontend) Concept

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:

Architeture without BFF

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

I will enter on project created, install the dependencies (I will use yarn) and run and application

cd webapp
yarn
yarn dev
Enter fullscreen mode Exit fullscreen mode

So here we are, now we have our web application:

Vite Screen using React and Typescript

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Now we have this screen:

New screeen with an input and a button to fetch

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);
  };
Enter fullscreen mode Exit fullscreen mode

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.

Response of requests

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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 };
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now let's start our application using:

yarn start
Enter fullscreen mode Exit fullscreen mode

Now if we access the endpoint: http://localhost:3000/data/1

We will receive the same data:

Server Response

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);
  };
Enter fullscreen mode Exit fullscreen mode

And let's see the same result:

With BFF

But now the BFF is responsible to fetch the data and manipulate for our clients (web application, desktop, mobile app).

Architeture with BFF

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!!!

Thank you gif

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/

Top comments (2)

Collapse
 
luizlahr profile image
Luiz Lahr

Correct me if I am wrong, but here, you will need to wait for 5 calls instead of 4, since you will need to wait for the response of your "1" call to the backend. Depending on how far your backend is sitting from your front end that would increase even more response time.

It may not been the best example since the API calls you made there could have been done async instead of stacked which would have increased the speed of the response.

Anyway, IMHO I would prefer to handle the API calls asynchronously on the front end and apply some other strategy such as loading states, skeletons, suspense, Tanstack Query caching the calls, etc. so that you can still have some content displayed making it a good experience for the user.

Collapse
 
jangelodev profile image
João Angelo

Hi Kevin Toshihiro Uehara,
Your tips are very useful
Thanks for sharing