Explore how to manage multiple backends, databases, and API calls within a single frontend application using Redux. This article breaks down practical patterns for handling asynchronous data flows, maintaining state consistency, and synchronizing data across diverse services reflecting the architecture commonly used in large-scale, real-world applications.
Modern applications often use multiple backends and databases. A frontend (React, Angular, etc.) may need to fetch and combine data from different APIs and keep everything synchronized in state management like Redux.
A. Understand the Architecture
A typical architecture with multiple data sources looks like this:
Frontend (React)
|
|------ API 1 ------ Backend A ------ Database A
|
|------ API 2 ------ Backend B ------ Database B
|
|------ API 3 ------ Backend C ------ Database C
Example scenario:
| Database | Data |
|---|---|
| Database A | Players |
| Database B | Sports |
| Database C | Player-Sports relationships |
The frontend must combine all data to display meaningful information.
Example output in UI:
MS Dhoni
Sports:
- Cricket
- Football
B. Design the Database Properly
A normalized relational structure is essential.
Players Table
players
--------------------------------
id | name
1 | MS Dhoni
2 | Virat Kohli
Sports Table
sports
--------------------------------
id | sport_name
1 | Cricket
2 | Football
3 | Golf
Player_Sports Table (Relationship Table)
player_sports
--------------------------------
player_id | sport_id
1 | 1
1 | 2
2 | 1
This design allows:
- One player → multiple sports
- One sport → multiple players
This is called a many-to-many relationship.
C. Create Backend APIs
Each backend service exposes its own API.
Example endpoints:
GET /api/players
GET /api/sports
GET /api/player-sports
Example responses:
/players
[
{ "id": 1, "name": "MS Dhoni" },
{ "id": 2, "name": "Virat Kohli" }
]
/sports
[
{ "id": 1, "sport": "Cricket" },
{ "id": 2, "sport": "Football" }
]
/player-sports
[
{ "player_id": 1, "sport_id": 1 },
{ "player_id": 1, "sport_id": 2 }
]
D. Setup Redux Store Structure
Instead of storing everything together, keep normalized data.
Example Redux store structure:
store
├ players
├ sports
└ playerSports
Example state:
{
players: [],
sports: [],
playerSports: []
}
This makes updates and caching easier.
E. Create Redux Slices
Use Redux Toolkit.
Players Slice
const playersSlice = createSlice({
name: "players",
initialState: [],
reducers: {
setPlayers: (state, action) => action.payload
}
})
F. Fetch Multiple APIs
Use asyncThunk to fetch data.
Example:
export const fetchPlayers = createAsyncThunk(
"players/fetch",
async () => {
const res = await fetch("/api/players")
return res.json()
}
)
Similarly create:
fetchSports()
fetchPlayerSports()
G. Dispatch API Calls Together
When the page loads, call all APIs.
Example:
useEffect(() => {
dispatch(fetchPlayers())
dispatch(fetchSports())
dispatch(fetchPlayerSports())
}, [])
Now Redux contains all datasets.
H. Combine Data in Selectors
Selectors are used to merge datasets.
Example:
const playersWithSports = players.map(player => {
const sportsPlayed = playerSports
.filter(ps => ps.player_id === player.id)
.map(ps => sports.find(s => s.id === ps.sport_id))
return {
...player,
sports: sportsPlayed
}
})
Result:
[
{
name: "MS Dhoni",
sports: ["Cricket", "Football"]
}
]
I. Render Data in React
Example:
playersWithSports.map(player => (
<div key={player.id}>
<h2>{player.name}</h2>
<ul>
{player.sports.map(s => (
<li key={s.id}>{s.sport}</li>
))}
</ul>
</div>
))
J. Avoid Too Many API Calls (Optimization)
Large applications often use a Backend for Frontend (BFF) or API Gateway.
Architecture:
Frontend
|
API Gateway
|
---------------------------------
| | |
Service A Service B Service C
The gateway merges responses.
Example response:
{
"name": "MS Dhoni",
"sports": ["Cricket", "Football"]
}
Benefits:
- fewer API calls
- faster UI
- cleaner frontend code
K. Advanced Redux Optimization
Use:
- RTK Query
- Memoized selectors
- Normalized state
Example normalized state:
entities
├ playersById
├ sportsById
└ playerSports
Benefits:
- faster lookups
- less re-rendering
- scalable architecture
L. Common Problems & Solutions
Problem 1: API loads at different times
Solution:
loading states per slice
Problem 2: Duplicate data
Solution:
Normalize state
Problem 3: Slow frontend
Solution:
- Use API gateway
- Cache responses
- Use RTK Query
M. Real-World Examples
Companies using similar architecture:
| Company | System |
|---|---|
| Netflix | Microservices APIs |
| GraphQL gateway | |
| Amazon | Service-based APIs |
Frontend merges multiple services.
N. Final Architecture Summary
Now let’s move to the practical production-level guide for large frontend systems that consume many APIs and multiple databases, including:
- Real production folder structure
- How large apps manage 20+ APIs efficiently
- Replacing Redux with RTK Query / React Query
- Microservices architecture used by big tech
A. Real Production Frontend Folder Structure
A scalable frontend usually follows feature-based architecture, not file-type architecture.
Basic Small Project (Not Scalable ❌)
src
├ components
├ pages
├ redux
├ api
└ utils
This becomes messy with 20+ APIs.
Production-Level Structure (Recommended)
src
├ app
│ ├ store.js
│ └ hooks.js
│
├ services
│ ├ apiClient.js
│ ├ playersApi.js
│ ├ sportsApi.js
│ └ authApi.js
│
├ features
│ ├ players
│ │ ├ playersSlice.js
│ │ ├ playersSelectors.js
│ │ ├ PlayersPage.jsx
│ │ └ components
│ │ └ PlayerCard.jsx
│ │
├ sports
│ │ ├ sportsSlice.js
│ │ └ SportsPage.jsx
│ │
├ auth
│ ├ authSlice.js
│ └ LoginPage.jsx
│
├ components
│ ├ UI
│ ├ layout
│ └ common
│
├ hooks
│ ├ useAuth.js
│ └ useFetchPlayers.js
│
├ utils
│ ├ helpers.js
│ └ constants.js
│
├ routes
│ └ AppRoutes.jsx
│
└ index.js
Benefits:
- Each feature owns its logic
- Easier scaling
- Easy onboarding for new developers
- APIs are isolated in services
B. How Large Apps Manage 20+ APIs Efficiently
Large apps don't call APIs randomly.
They use 3 key layers.
API Service Layer
Create a central API client.
services/apiClient.js
Example:
import axios from "axios"
const apiClient = axios.create({
baseURL: "/api",
timeout: 10000
})
export default apiClient
Separate APIs by domain
Example:
services
├ usersApi.js
├ ordersApi.js
├ playersApi.js
├ sportsApi.js
└ analyticsApi.js
Example:
import api from "./apiClient"
export const getPlayers = () => api.get("/players")
export const getSports = () => api.get("/sports")
Benefits
With 20+ APIs:
| Problem | Solution |
|---|---|
| Messy API calls | API service layer |
| Duplicate code | Shared API client |
| Difficult caching | Query libraries |
C. Replace Redux with RTK Query
Redux Toolkit introduced RTK Query for API state.
Advantages:
✔ Automatic caching
✔ Automatic refetching
✔ No reducers needed
✔ No thunks needed
Example RTK Query Setup
services/apiSlice.js
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"
export const api = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
endpoints: (builder) => ({
getPlayers: builder.query({
query: () => "/players"
}),
getSports: builder.query({
query: () => "/sports"
})
})
})
export const { useGetPlayersQuery, useGetSportsQuery } = api
Using It in Components
const { data: players, isLoading } = useGetPlayersQuery()
No reducers needed.
D. Using React Query Instead
Many companies prefer React Query (TanStack Query).
Example:
import { useQuery } from "@tanstack/react-query"
import axios from "axios"
const fetchPlayers = async () => {
const res = await axios.get("/api/players")
return res.data
}
export const usePlayers = () =>
useQuery({
queryKey: ["players"],
queryFn: fetchPlayers
})
Usage:
const { data, isLoading } = usePlayers()
Why React Query is popular
| Feature | Benefit |
|---|---|
| Caching | automatic |
| Background refresh | automatic |
| Deduping | automatic |
| Pagination | easy |
E. Backend Microservices Architecture
Large companies use microservices instead of one backend.
Example architecture:
Each service has its own database.
This is called:
Database per service pattern
Used by companies like:
- Netflix
- Amazon
- Uber
- Airbnb
F. API Gateway Pattern
Frontend should not call 20 services directly.
Instead use:
Benefits:
✔ fewer API calls
✔ better security
✔ central auth
✔ response aggregation
G. Backend for Frontend (BFF)
Large systems often create a BFF layer.
Example:
Frontend (Web)
Frontend (Mobile)
|
BFF Layer
|
Microservices
Each frontend gets custom API responses.
Used heavily by:
- Netflix
- Spotify
H. Example End-to-End Architecture
I. Performance Techniques Used by Big Tech
Caching
Redis
CDN
Browser Cache
Request batching
Instead of:
10 API calls
Use:
1 aggregated API
GraphQL
Many companies use GraphQL to fetch only needed data.
Example:
query {
player {
name
sports
}
}
Used by:
- Meta Platforms
- Shopify
J. Final Best Production Stack
Modern frontend stack:
React
React Query / RTK Query
TypeScript
Axios
Feature-based architecture
API Gateway backend
Microservices
Now let us move to next step
- How to design a frontend that scales to 1 million users
- How to manage 100+ APIs without chaos
Managing 100+ APIs is a real challenge in large-scale systems. Companies like Netflix, Amazon, and Uber solve this with structured architecture, tooling, and conventions. Below is a practical production approach used in large applications.
A. Organize APIs by Domain (Domain-Driven Design)
Never keep all APIs in one folder.
Instead group them by business domain.
Bad structure ❌
services
├ api1.js
├ api2.js
├ api3.js
├ api4.js
└ api100.js
Impossible to maintain.
Correct structure ✅
services
├ auth
│ ├ authApi.js
│ └ authTypes.js
│
├ users
│ ├ usersApi.js
│ └ usersTypes.js
│
├ orders
│ ├ ordersApi.js
│ └ ordersTypes.js
│
├ payments
│ ├ paymentsApi.js
│ └ paymentsTypes.js
│
├ analytics
│ ├ analyticsApi.js
│ └ analyticsTypes.js
Each domain owns its APIs.
Benefits:
Easier maintenance
Clear ownership
Smaller modules
B. Use a Central API Client
Avoid repeating configuration.
Create a single HTTP client.
// apiClient.js
import axios from "axios"
const apiClient = axios.create({
baseURL: "/api",
timeout: 10000
})
apiClient.interceptors.request.use(config => {
const token = localStorage.getItem("token")
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
export default apiClient
Now all APIs use the same client.
C. API Layer Pattern
Never call APIs directly in components.
Correct flow:
Example:
Service
// usersApi.js
import apiClient from "../apiClient"
export const getUsers = () => apiClient.get("/users")
export const getUser = (id) => apiClient.get(`/users/${id}`)
Hook
// useUsers.js
import { useQuery } from "@tanstack/react-query"
import { getUsers } from "../services/users/usersApi"
export const useUsers = () =>
useQuery({
queryKey: ["users"],
queryFn: getUsers
})
D. Use React Query or RTK Query for Caching
When managing 100 APIs, manual state becomes impossible.
Libraries like:
React Query
RTK Query
handle:
✔ caching
✔ deduplication
✔ background refresh
✔ pagination
✔ loading states
Example:
const { data, isLoading } = useQuery({
queryKey: ["players"],
queryFn: fetchPlayers
})
E. API Versioning
Large systems need version control.
Example:
/api/v1/users
/api/v1/orders
/api/v2/users
This prevents breaking old clients.
F. Use an API Gateway
Frontend should not talk to 100 services directly.
Use an API Gateway.
Architecture:
The gateway:
✔ aggregates responses
✔ handles authentication
✔ rate limits
✔ routing
This pattern is heavily used by Netflix.
G. Backend for Frontend (BFF)
Different frontends need different responses.
Example:
Web Frontend
Mobile App
Admin Panel
Each uses a BFF service.
Benefits:
optimized responses
fewer API calls
faster UI
H. API Documentation & Discovery
When you have 100 APIs, developers must discover them easily.
Use tools like:
OpenAPI / Swagger
API catalogs
internal developer portals
Example:
/api-docs
This shows:
endpoints
parameters
Responses
I. Naming Conventions
Consistency prevents chaos. Use standard naming patterns to ensure clarity across multiple teams and services.
| RESTful Convention (Good) | Non-RESTful (Avoid) |
|---|---|
| GET /users | /fetchUsersList |
| GET /users/{id} | /getUser |
| POST /users | /createUserRecord |
| PUT /users/{id} | /updateUser |
| DELETE /users/{id} | /removeUser |
J. Request Aggregation
Instead of:
5 API calls
Use:
1 aggregated endpoint
Example response:
{
"user": {...},
"orders": [...],
"payments": [...]
}
This reduces network overhead.
K. Use Feature-Based Frontend Architecture
Each feature manages its own APIs.
Example:
features
├ users
│ ├ usersApi.js
│ ├ useUsers.js
│ └ UsersPage.jsx
│
├ orders
│ ├ ordersApi.js
│ ├ useOrders.js
│ └ OrdersPage.jsx
This keeps large apps maintainable.
L. Monitoring & Observability
For 100+ APIs you must monitor everything.
Common tools:
logging systems
distributed tracing
performance dashboards
Large companies rely heavily on these.
M. Rate Limiting & Security
With many APIs you must protect them.
Common protections:
✔ API keys
✔ OAuth tokens
✔ rate limits
✔ request validation
N. Testing Strategy
Every API should have:
- unit tests
- integration tests
- contract tests
Automated pipelines verify APIs before deployment.
Final Architecture Example
Key Principles for Managing 100+ APIs
- Domain-based organization
- Central API client
- Service layer abstraction
- Query library for caching
- API gateway
- Backend-for-frontend pattern
- Strong documentation
Managing complexity in multi-source frontend applications requires a shift from simple data fetching to a structured architectural approach. By implementing patterns like an API Gateway or Backend for Frontend (BFF), developers can aggregate disparate data sources and reduce network overhead. Furthermore, leveraging modern tooling such as React Query or RTK Query automates critical tasks like caching and state synchronization. Ultimately, these practices ensure that large-scale systems remain both scalable and maintainable as the number of services and APIs continues to grow.










Top comments (0)