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

  1. Не дублируйте бизнес-логику — она должна быть в микросервисах
  2. Кэшируйте агрегированные данные — уменьшайте нагрузку на микросервисы
  3. Используйте GraphQL — для гибкости запросов
  4. Версионируйте API — для плавных обновлений клиентов
  5. Мониторьте производительность — отслеживайте латентность каждого BFF

Заключение

BFF паттерн позволяет оптимизировать взаимодействие между клиентами и микросервисами, предоставляя каждому типу клиента именно те данные и в том формате, которые ему нужны.