Реализация BFF (Backend for Frontend) паттерна
BFF — паттерн, при котором для каждого типа клиента (web, mobile, TV-приложение) создаётся отдельный бэкенд-слой. Вместо одного универсального 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`)
]);
// Агрегация с обработкой partial failures
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, 'RUB'),
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 недели







