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
-
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.
-
Reduced Complexity:
- Instead of addressing the needs of all clients in a single backend, BFF offers a structure tailored to each client.
-
Modularity and Scalability:
- Since each client has its own backend layer, development processes are more modular.
-
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
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"]
}
]
}
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}`));
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}`));
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;
};
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;
};
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);
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>
);
};
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="/" />;
};
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" />
}
/>
);
};
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
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:
-
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)
These commands ensure that requests made to http://localhost:5000
from your mobile device are correctly routed to the server running on your computer.
- Verify the Port Forwarding: Run this command to list the currently active port redirections:
adb reverse --list
The output will confirm the active mappings, like:
5000 -> 5000
3001 -> 3001
- Start Your Backend Services: Ensure your backend services (e.g., Mobile BFF) are running on the specified ports:
make start-backend-mobile-bff
-
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:
Custom Backend Endpoints:
Each client type (web, mobile) communicates with its dedicated backend layer, ensuring optimized data delivery.Sustainable UI Development:
Adopting atomic design principles fosters reusable, maintainable, and scalable UI components.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)