microservices
Паттерн Backend for Frontend (BFF)
Создание специализированных backend сервисов для разных типов клиентов
•
#microservices
#patterns
#bff
#api-design
Паттерн Backend for Frontend (BFF)
Backend for Frontend (BFF) — архитектурный паттерн, при котором для каждого типа клиента создается отдельный backend сервис, оптимизированный под его специфические потребности.
Проблема
Универсальный API
// Один API для всех клиентов
class UniversalAPI {
async getProduct(id: string) {
return {
id,
name: 'Product',
description: 'Long description...',
images: ['img1.jpg', 'img2.jpg', 'img3.jpg'],
specifications: { /* много данных */ },
reviews: [ /* массив отзывов */ ],
relatedProducts: [ /* связанные товары */ ],
inventory: { /* данные о наличии */ }
};
}
}
// Проблемы:
// - Мобильное приложение получает слишком много данных
// - Веб-клиент делает множество запросов
// - Smart TV нужен другой формат
Решение с BFF
Архитектура
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Web App │────▶│ Web BFF │ │ │
└─────────────┘ └──────────────┘ │ │
│ │ │
┌─────────────┐ ┌──────────────┐ │ Micro- │
│ Mobile App │────▶│ Mobile BFF │────▶│ services │
└─────────────┘ └──────────────┘ │ │
│ │ │
┌─────────────┐ ┌──────────────┐ │ │
│ Smart TV │────▶│ TV BFF │ │ │
└─────────────┘ └──────────────┘ └─────────────┘
Реализация
Web BFF
class WebBFF {
constructor(
private productService: ProductService,
private reviewService: ReviewService,
private inventoryService: InventoryService
) {}
async getProductPage(productId: string) {
// Агрегируем данные для веб-страницы
const [product, reviews, inventory, related] = await Promise.all([
this.productService.getProduct(productId),
this.reviewService.getReviews(productId, { limit: 10 }),
this.inventoryService.getAvailability(productId),
this.productService.getRelatedProducts(productId, { limit: 6 })
]);
return {
product: {
...product,
// Полное описание для веба
description: product.fullDescription,
images: product.images.map(img => ({
url: img.url,
thumbnail: img.thumbnail,
alt: img.alt
}))
},
reviews: {
items: reviews,
summary: this.calculateReviewSummary(reviews)
},
inventory: {
inStock: inventory.quantity > 0,
quantity: inventory.quantity,
estimatedDelivery: this.calculateDelivery(inventory)
},
relatedProducts: related.map(p => this.formatRelatedProduct(p))
};
}
}
Mobile BFF
class MobileBFF {
constructor(
private productService: ProductService,
private reviewService: ReviewService,
private inventoryService: InventoryService
) {}
async getProductPage(productId: string) {
// Оптимизировано для мобильных устройств
const [product, reviewSummary, inventory] = await Promise.all([
this.productService.getProduct(productId),
this.reviewService.getSummary(productId),
this.inventoryService.getAvailability(productId)
]);
return {
product: {
id: product.id,
name: product.name,
// Краткое описание для мобильных
description: this.truncate(product.description, 200),
price: product.price,
// Только главное изображение
image: product.images[0]?.thumbnail,
// Минимальные характеристики
keySpecs: product.specifications.slice(0, 3)
},
reviews: {
averageRating: reviewSummary.average,
totalCount: reviewSummary.count
},
availability: inventory.quantity > 0 ? 'in-stock' : 'out-of-stock'
};
}
private truncate(text: string, length: number): string {
return text.length > length
? text.substring(0, length) + '...'
: text;
}
}
TV BFF
class TVBFF {
constructor(
private productService: ProductService,
private mediaService: MediaService
) {}
async getProductPage(productId: string) {
const [product, media] = await Promise.all([
this.productService.getProduct(productId),
this.mediaService.getProductMedia(productId)
]);
return {
product: {
id: product.id,
name: product.name,
// Крупный текст для TV
description: this.formatForTV(product.description),
price: this.formatPrice(product.price),
// Высококачественные изображения для больших экранов
images: media.images.map(img => img.highRes),
// Видео контент
videos: media.videos.map(v => ({
url: v.url,
thumbnail: v.thumbnail,
duration: v.duration
}))
},
// Упрощенная навигация для пульта
navigation: {
next: this.getNextProduct(productId),
previous: this.getPreviousProduct(productId),
related: this.getRelatedProducts(productId, { limit: 4 })
}
};
}
}
Преимущества BFF
1. Оптимизация под клиента
// Каждый BFF возвращает именно то, что нужно клиенту
interface WebProductResponse {
product: DetailedProduct;
reviews: PaginatedReviews;
recommendations: Product[];
seo: SEOMetadata;
}
interface MobileProductResponse {
product: CompactProduct;
reviewSummary: ReviewSummary;
quickActions: Action[];
}
2. Независимая эволюция
// Web BFF может добавлять новые фичи независимо
class WebBFF {
// Новая фича только для веба
async getProductWithAR(productId: string) {
const product = await this.getProductPage(productId);
const arModel = await this.arService.getModel(productId);
return {
...product,
ar: {
modelUrl: arModel.url,
viewerConfig: arModel.config
}
};
}
}
3. Упрощение клиентского кода
// Клиент делает один запрос вместо нескольких
class MobileApp {
async showProductPage(productId: string) {
// Один запрос к BFF
const data = await this.mobileBFF.getProductPage(productId);
// Все данные уже в нужном формате
this.render(data);
}
}
Реализация общей логики
Shared Services Layer
// Базовый класс для всех BFF
abstract class BaseBFF {
constructor(
protected productService: ProductService,
protected userService: UserService,
protected logger: Logger
) {}
protected async enrichWithUserData<T>(
data: T,
userId: string
): Promise<T & { userContext: UserContext }> {
const userContext = await this.userService.getContext(userId);
return { ...data, userContext };
}
protected handleError(error: Error): never {
this.logger.error('BFF Error', error);
throw new BFFError(error.message);
}
}
// Конкретные BFF наследуются от базового
class WebBFF extends BaseBFF {
async getProductPage(productId: string, userId: string) {
const data = await this.fetchProductData(productId);
return this.enrichWithUserData(data, userId);
}
}
Shared Utilities
// Общие утилиты для всех BFF
class BFFUtils {
static async aggregateData<T>(
promises: Promise<T>[],
options: { timeout?: number; fallback?: T }
): Promise<T[]> {
try {
return await Promise.all(promises);
} catch (error) {
if (options.fallback) {
return [options.fallback];
}
throw error;
}
}
static formatCurrency(amount: number, currency: string): string {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency
}).format(amount);
}
}
GraphQL как альтернатива
// GraphQL позволяет клиенту запрашивать нужные данные
const MOBILE_PRODUCT_QUERY = gql`
query GetProduct($id: ID!) {
product(id: $id) {
id
name
description(maxLength: 200)
price
mainImage: images(limit: 1) {
thumbnail
}
reviewSummary {
average
count
}
}
}
`;
const WEB_PRODUCT_QUERY = gql`
query GetProduct($id: ID!) {
product(id: $id) {
id
name
fullDescription
price
images {
url
thumbnail
alt
}
reviews(limit: 10) {
author
rating
text
}
relatedProducts(limit: 6) {
id
name
price
}
}
}
`;
Мониторинг и метрики
class MonitoredBFF {
constructor(
private metrics: MetricsCollector,
private tracer: Tracer
) {}
async getProductPage(productId: string) {
const span = this.tracer.startSpan('bff.getProductPage');
const startTime = Date.now();
try {
const result = await this.fetchData(productId);
this.metrics.recordLatency(
'bff.getProductPage',
Date.now() - startTime
);
return result;
} catch (error) {
this.metrics.incrementCounter('bff.errors');
throw error;
} finally {
span.finish();
}
}
}
Best Practices
- Не дублируйте бизнес-логику — она должна быть в микросервисах
- Кэшируйте агрегированные данные — уменьшайте нагрузку на микросервисы
- Используйте GraphQL — для гибкости запросов
- Версионируйте API — для плавных обновлений клиентов
- Мониторьте производительность — отслеживайте латентность каждого BFF
Заключение
BFF паттерн позволяет оптимизировать взаимодействие между клиентами и микросервисами, предоставляя каждому типу клиента именно те данные и в том формате, которые ему нужны.