Реалізація BFF (Backend for Frontend) паттерна
BFF — паттерн, при якому для кожного типу клієнта (web, mobile, TV-додаток) створюється окремий backend-шар. Замість одного універсального API, яке намагається задовольнити всіх клієнтів одночасно, кожен BFF повертає дані у формі, оптимальній для свого клієнта.
Проблема без BFF
Мобільний додаток робить 5 запитів щоб показати один екран профілю:
-
GET /users/{id}— базові дані -
GET /orders?userId={id}&limit=3— останні замовлення -
GET /notifications/unread— лічильник сповіщень -
GET /recommendations?userId={id}— рекомендації -
GET /loyalty/points/{id}— бали лояльності
Кожен запит — окремий round trip. На мобільній мережі це 500–1500ms сумарно.
Рішення з BFF
Mobile BFF Web BFF
(Node.js) (Node.js)
iOS App ──────────► /mobile/dashboard │
Android App ───────► │ │
│ │
├─► User Service ◄─── Web SPA
├─► Order Service ◄──────────
├─► Notification ◄──────────
└─► Recommendation ◄──────────
Реалізація Mobile BFF
// mobile-bff/routes/dashboard.ts
router.get('/mobile/dashboard', authenticate, async (req, res) => {
const userId = req.user.id;
// Паралельні запити до мікросервісів
const [userResult, ordersResult, notificationsResult, loyaltyResult] =
await Promise.allSettled([
userService.get(`/users/${userId}`),
orderService.get(`/orders?customerId=${userId}&limit=3&fields=id,status,total,createdAt`),
notificationService.get(`/notifications/${userId}/unread-count`),
loyaltyService.get(`/loyalty/${userId}/summary`)
]);
// Агрегація з обробкою часткових невдач
const response = {
user: userResult.status === 'fulfilled' ? {
id: userResult.value.data.id,
name: userResult.value.data.displayName,
avatar: userResult.value.data.avatarUrl
} : null,
recentOrders: ordersResult.status === 'fulfilled'
? ordersResult.value.data.items.map(transformOrderForMobile)
: [],
unreadCount: notificationsResult.status === 'fulfilled'
? notificationsResult.value.data.count
: 0,
loyalty: loyaltyResult.status === 'fulfilled' ? {
points: loyaltyResult.value.data.balance,
tier: loyaltyResult.value.data.tier
} : null
};
res.json(response);
});
// Трансформація даних під мобільний UI
function transformOrderForMobile(order: Order): MobileOrder {
return {
id: order.id,
status: localizeStatus(order.status), // 'Доставлено' замість 'DELIVERED'
total: formatCurrency(order.total, 'UAH'),
date: formatRelativeDate(order.createdAt) // '2 дні тому'
};
}
Web BFF — інший формат для тих же даних
// web-bff/routes/dashboard.ts
router.get('/web/dashboard', authenticate, async (req, res) => {
const userId = req.user.id;
// Web-версія запитує більше даних для багатого UI
const [user, orders, stats, notifications] = await Promise.allSettled([
userService.get(`/users/${userId}`),
orderService.get(`/orders?customerId=${userId}&limit=10`),
analyticsService.get(`/analytics/user/${userId}/stats`),
notificationService.get(`/notifications/${userId}?limit=5&unread=true`)
]);
// Web-формат — більше даних, інша структура
res.json({
user: user.status === 'fulfilled' ? user.value.data : null,
orders: orders.status === 'fulfilled' ? orders.value.data : { items: [], total: 0 },
analytics: stats.status === 'fulfilled' ? stats.value.data : null,
notifications: notifications.status === 'fulfilled' ? notifications.value.data : []
});
});
GraphQL BFF
Якщо клієнт — React-додаток з Apollo Client, BFF може експортувати GraphQL:
// web-bff/graphql/schema.ts
const typeDefs = gql`
type Query {
dashboard: Dashboard!
order(id: ID!): Order
}
type Dashboard {
user: User!
recentOrders: [Order!]!
stats: UserStats!
}
`;
const resolvers = {
Query: {
dashboard: async (_, __, { userId }) => {
const [user, orders, stats] = await Promise.all([
userService.getUser(userId),
orderService.getRecentOrders(userId),
analyticsService.getUserStats(userId)
]);
return { user, recentOrders: orders, stats };
}
}
};
Авторизація та аутентифікація в BFF
BFF — природне місце для перевірки JWT та управління сесіями. Особливо для браузерного клієнта:
// BFF зберігає refresh token у httpOnly cookie,
// не передаючи його в браузер JavaScript
router.post('/auth/refresh', async (req, res) => {
const refreshToken = req.cookies.refresh_token;
if (!refreshToken) return res.status(401).json({ error: 'No token' });
const tokens = await authService.refreshTokens(refreshToken);
res.cookie('refresh_token', tokens.refreshToken, {
httpOnly: true, secure: true, sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 днів
});
res.json({ accessToken: tokens.accessToken });
});
Кешування в BFF
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
async function getCachedOrFetch<T>(
key: string,
ttl: number,
fetcher: () => Promise<T>
): Promise<T> {
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const data = await fetcher();
await redis.setex(key, ttl, JSON.stringify(data));
return data;
}
// Рекомендації кешуємо на 5 хвилин
const recommendations = await getCachedOrFetch(
`recommendations:${userId}`,
300,
() => recommendationService.get(`/recommendations/${userId}`)
);
Терміни реалізації
- BFF для одного клієнта (3–5 агрегуючих ендпоінтів) — 1–2 тижні
- GraphQL BFF + авторизація + кешування — 2–3 тижні
- BFF для 2–3 клієнтів з спільною бібліотекою викликів сервісів — 3–4 тижні







