DEV Community

Cover image for Backend for Frontend (BFF) Architecture πŸš€
AbdulnasΔ±r Olcan
AbdulnasΔ±r Olcan

Posted on

Backend for Frontend (BFF) Architecture πŸš€

Modern software architectures aim to optimize user experiences across various platforms (web, mobile, IoT, etc.). However, meeting the diverse requirements of different devices through a single backend system can be challenging. The Backend for Frontend (BFF) architecture provides an effective solution to these challenges. By offering a dedicated backend layer for each client type, BFF enables a more tailored and performant approach.

This article will provide an in-depth analysis of BFF architecture, from its fundamental concepts to advanced implementations, illustrating how to apply it to your projects with examples. We will focus on a multi-backend architecture supporting real-world use cases, particularly React (Web), React Native (Mobile), and multiple backend services.

What is BFF and Why Use It?

Backend-for-Frontend is an architecture that proposes creating dedicated backend layers for each frontend client. In traditional architectures, all clients interact with a single API. However, since each client has distinct requirements, this approach can lead to complications. BFF addresses these issues by creating separate backends for each client type.

Advantages of BFF

  1. Performance Optimization:

    • Web and mobile clients have different data needs. When a single API provides all the data, clients may carry unnecessary payloads. BFF ensures only the required data is served.
  2. Reduced Complexity:

    • Instead of addressing the needs of all clients in a single backend, BFF offers a structure tailored to each client.
  3. Modularity and Scalability:

    • Since each client has its own backend layer, development processes are more modular.
  4. Security:

    • The BFF layer can act as a security shield between the client and the main backend.

Project Architecture

Below is an example project structure featuring React (web), React Native (mobile), and a Node.js-based backend:

πŸ“¦ backend
 ┣ πŸ“‚ _data
 ┃ β”— πŸ“œ db.json        # JSON file for mock data
 ┣ πŸ“‚ mobile-bff       # BFF tailored for the mobile client
 ┣ πŸ“‚ web-bff          # BFF tailored for the web client
 ┣ πŸ“‚ shared           # Shared backend code (e.g., validation, error handling)
 β”— πŸ“œ package.json     # Backend dependencies

πŸ“¦ frontend
 ┣ πŸ“‚ mobileApp        # React Native application
 ┃ ┣ πŸ“‚ app
 ┃ ┃ ┣ πŸ“‚ Components   # UI components (following atomic design principles)
 ┃ ┃ ┣ πŸ“‚ Screens      # Login, RecipeList, RecipeDetail
 ┃ ┃ ┣ πŸ“‚ Context      # Global states (e.g., AuthContext)
 ┃ ┃ β”— πŸ“‚ Navigation   # React Navigation configuration
 ┣ πŸ“‚ web              # React (web) application
 ┃ ┣ πŸ“‚ Components     # Atomic components
 ┃ ┣ πŸ“‚ Pages          # Login, RecipeList, RecipeDetail
 ┃ ┣ πŸ“‚ Context        # Global states for web
 ┃ β”— πŸ“œ index.tsx      # Entry point for React application
 β”— πŸ“œ package.json     # Frontend dependencies

πŸ“¦ node_modules         # Project dependencies
πŸ“œ Makefile             # Automation scripts
πŸ“œ package.json         # Root dependencies
πŸ“œ README.md            # Project documentation
πŸ“œ yarn.lock            # Dependency version locking
Enter fullscreen mode Exit fullscreen mode

Step-by-Step Implementation

1. Building the Backend Layer

The core backend serves as the central point for business logic and data processing. The BFF layers interact with this backend to provide customized data to clients.

Below is the backend structure and implementation:

Backend Structure:

  • _data/db.json: Example JSON data source:
{
  "recipes": [
    {
      "id": 1,
      "name": "Classic Margherita Pizza",
      "ingredients": [
        "Pizza dough",
        "Tomato sauce",
        "Fresh mozzarella cheese",
        "Fresh basil leaves",
        "Olive oil",
        "Salt and pepper to taste"
      ],
      "instructions": [
        "Preheat the oven to 475Β°F (245Β°C).",
        "Roll out the pizza dough and spread tomato sauce evenly.",
        "Top with slices of fresh mozzarella and fresh basil leaves.",
        "Drizzle with olive oil and season with salt and pepper.",
        "Bake in the preheated oven for 12-15 minutes or until the crust is golden brown.",
        "Slice and serve hot."
      ],
      "prepTimeMinutes": 20,
      "cookTimeMinutes": 15,
      "servings": 4,
      "difficulty": "Easy",
      "cuisine": "Italian",
      "caloriesPerServing": 300,
      "tags": ["Pizza", "Italian"],
      "userId": 166,
      "image": "recipe-images/1.webp",
      "rating": 4.6,
      "reviewCount": 98,
      "mealType": ["Dinner"]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Mobile BFF Example

The Mobile BFF acts as an intermediary layer between the core backend and the mobile client, tailoring responses for mobile-specific needs.

Structure: backend/mobile-bff/index.ts

import express from 'express';
import cors from 'cors';
import { recipes } from '../_data/db.json';

const app = express();
app.use(cors());

// Mobile-specific API route
app.get('/recipes', (req, res) => {
  // Example: Limiting the number of fields sent to mobile
  const minimalRecipes = recipes.map(({ id, name, image, rating, instructions }) => ({
    id,
    name,
    image,
    rating,
    description: instructions[0], // Only send the first instruction
  }));
  res.json(minimalRecipes);
});

const PORT = 3001;
app.listen(PORT, () => console.log(`Mobile BFF running on port ${PORT}`));
Enter fullscreen mode Exit fullscreen mode

Web BFF Example

The Web BFF is structured similarly but caters to the needs of the web client.

Structure: backend/web-bff/index.ts

import express from 'express';
import cors from 'cors';
import { recipes } from '../_data/db.json';

const app = express();
app.use(cors());

// Web-specific API route
app.get('/recipes', (req, res) => {
  // Example: Sending all fields for a detailed web experience
  res.json(recipes);
});

const PORT = 3002;
app.listen(PORT, () => console.log(`Web BFF running on port ${PORT}`));
Enter fullscreen mode Exit fullscreen mode

2. Frontend Integration

Both React and React Native frontends will consume their respective BFF APIs.

Web Integration:

React (Web) fetches data from the Web BFF:

Structure: frontend/web/src/services/recipe-service.ts

import axios from 'axios';

const API_URL = 'http://localhost:3002';

export const fetchRecipes = async () => {
  const response = await axios.get(`${API_URL}/recipes`);
  return response.data;
};
Enter fullscreen mode Exit fullscreen mode

Mobile Integration:

React Native fetches data from the Mobile BFF:

Structure: frontend/mobile/app/services/recipe-service.ts

import axios from 'axios';

const API_URL = 'http://localhost:3001';

export const fetchRecipes = async () => {
  const response = await axios.get(`${API_URL}/recipes`);
  return response.data;
};
Enter fullscreen mode Exit fullscreen mode

3. Global State Management with Context API

Auth Context:

Both web and mobile platforms use Context API to manage authentication states.

Structure: frontend/web/src/context/AuthContext.tsx

import React, { createContext, useState, useContext } from 'react';

const AuthContext = createContext(null);

export const AuthProvider = ({ children }) => {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  const login = () => setIsLoggedIn(true);
  const logout = () => setIsLoggedIn(false);

  return (
    <AuthContext.Provider value={{ isLoggedIn, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => useContext(AuthContext);
Enter fullscreen mode Exit fullscreen mode

4. Navigation and Guards

For Mobile: Use React Navigation for screen transitions, such as between RecipeListScreen and RecipeDetailScreen.

import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';

const Stack = createStackNavigator();

const AppNavigator = () => {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="RecipeList" component={RecipeListScreen} />
        <Stack.Screen name="RecipeDetail" component={RecipeDetailScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
};
Enter fullscreen mode Exit fullscreen mode

Route Guards:

import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';

// PrivateRoute
export const PrivateRoute: React.FC = () => {
    const { isAuthenticated } = useAuth();
    return isAuthenticated ? <Outlet /> : <Navigate to="/login" />;
};

// PublicRoute
export const PublicRoute: React.FC = () => {
    const { isAuthenticated } = useAuth();
    return !isAuthenticated ? <Outlet /> : <Navigate to="/" />;
};
Enter fullscreen mode Exit fullscreen mode

For Web: Use React Router with route guards.

import { BrowserRouter as Router, Route, Redirect } from 'react-router-dom';
import { useAuth } from './context/AuthContext';

const PrivateRoute = ({ component: Component, ...rest }) => {
  const { isLoggedIn } = useAuth();
  return (
    <Route
      {...rest}
      render={(props) =>
        isLoggedIn ? <Component {...props} /> : <Redirect to="/login" />
      }
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

Project Management

1. Makefile

A Makefile simplifies the management of multi-platform projects like this one by providing predefined commands to automate tasks such as installing dependencies, cleaning up, and running applications. Below is an example of a Makefile designed for this BFF architecture:

Makefile Example:

install:
 yarn install --frozen-lockfile

start-backends:
 concurrently "cd backend/web-bff && yarn start" "cd backend/mobile-bff && yarn start"

start-frontends:
 concurrently "cd frontend/web && yarn start" "cd frontend/mobile && yarn start"

clean:
 rm -rf node_modules backend/**/node_modules frontend/**/node_modules
Enter fullscreen mode Exit fullscreen mode

This approach ensures the project is modular, manageable, and ready for both development and production environments. It significantly reduces manual errors and speeds up common tasks.

2. Local API Redirection

When developing and testing a mobile application on a real device or emulator, it’s necessary to route API requests from the mobile application to the local backend server running on your development machine. The adb reverse command allows you to forward traffic from the device to your local machine, enabling seamless API testing.

Steps for API Redirection:

Android:
  1. Forward the Localhost Port Using adb reverse: Open your terminal and use the following commands to redirect traffic:
   adb reverse tcp:5000 tcp:5000  # Redirects Mobile BFF API (e.g., backend)
   adb reverse tcp:3001 tcp:3001  # Redirects another API port (if needed)
Enter fullscreen mode Exit fullscreen mode

These commands ensure that requests made to http://localhost:5000 from your mobile device are correctly routed to the server running on your computer.

  1. Verify the Port Forwarding: Run this command to list the currently active port redirections:
   adb reverse --list
Enter fullscreen mode Exit fullscreen mode

The output will confirm the active mappings, like:

   5000 -> 5000
   3001 -> 3001
Enter fullscreen mode Exit fullscreen mode
  1. Start Your Backend Services: Ensure your backend services (e.g., Mobile BFF) are running on the specified ports:
   make start-backend-mobile-bff
Enter fullscreen mode Exit fullscreen mode
  1. Update Mobile App Configuration: In your mobile app's configuration or environment variables, point the API base URL to http://localhost:5000. This ensures the app connects to the local backend during development.

By following these steps, your mobile app will seamlessly communicate with your local backend, enabling smooth development and debugging workflows.

Conclusion

The Backend for Frontend (BFF) architecture is an invaluable solution for addressing the unique needs of client applications in large-scale projects. By tailoring backend services to specific client requirements, it enhances both performance and development efficiency. In this guide, we implemented a complete example using React, React Native, and Node.js, building customized BFF layers for both mobile and web clients.

Project Benefits:

  1. Custom Backend Endpoints:
    Each client type (web, mobile) communicates with its dedicated backend layer, ensuring optimized data delivery.

  2. Sustainable UI Development:
    Adopting atomic design principles fosters reusable, maintainable, and scalable UI components.

  3. Streamlined Management:
    Tools like Makefile and Yarn make dependency management and development workflows straightforward and efficient.

Project Code

You can find the full project code, including all the examples and configurations discussed in this article, on the GitHub repository. Click the link below to explore, clone, and try it out yourself:

GitHub Repository: Backend-for-Frontend Architecture Project

Start building your next scalable project with BFF architecture today Happy coding! πŸš€

Top comments (0)