
BFF(Backend for Frontend)パターン実践ガイド
モバイルアプリと Web アプリで求められる API の形は異なります。モバイルは通信量を抑えたい、Web は SEO のためにサーバーサイドで HTML を生成したい。同じバックエンド API をそのまま使うと、クライアント側で複雑な加工処理が必要になり、パフォーマンスと保守性が低下します。
BFF(Backend for Frontend)パターンは、フロントエンド(クライアント)ごとに専用のバックエンド層を用意することで、この課題を解決します。本記事では、BFF の設計判断・実装パターン・運用設計まで、受託開発の現場で即使える実務知見を解説します。
1. BFF パターンとは何か
BFF の基本構造
BFF は、クライアント(モバイル・Web・管理画面など)ごとに専用の API 層を配置するアーキテクチャパターンです。
[Mobile App] → [Mobile BFF] ┐
[Web App] → [Web BFF] ├→ [共通 Microservices]
[Admin] → [Admin BFF] ┘ (User / Product / Order...)
BFF が解決する課題
| 課題 | BFF なし | BFF あり | |------|----------|----------| | データ形式の不一致 | クライアントが複数 API を呼び出して加工 | BFF が必要な形式で返却 | | 過剰なデータ取得 | 不要なフィールドも含めて返却 | クライアントに必要な項目だけ返却 | | 認証の複雑化 | 各クライアントが個別に認証処理 | BFF で統一的に認証・認可 | | バージョン管理 | 全クライアントで同じ API 仕様 | クライアントごとに独立して変更可能 |
BFF を採用すべきケース
- 複数のクライアント種別がある(iOS / Android / Web / 管理画面)
- クライアントごとに求められるデータ形式が異なる
- モバイルで通信量・バッテリー消費を最適化したい
- Web で SEO 対応のため SSR が必要
- マイクロサービス化が進み、クライアント側での API 統合が複雑
2. BFF と GraphQL の比較
BFF を検討する際、GraphQL も候補に挙がります。それぞれの特性を整理します。
比較表
| 観点 | BFF(REST ベース) | GraphQL | |------|-------------------|----------| | 学習コスト | 低い(既存の REST 知識が活用可能) | 中〜高(スキーマ言語・Resolver の理解が必要) | | クライアント自由度 | BFF が返すデータ形式は固定 | クライアントが必要なフィールドを指定可能 | | キャッシュ | HTTP キャッシュが使える | CDN キャッシュが難しい(POST リクエスト) | | N+1 問題 | BFF で事前に対策 | DataLoader で解決が必要 | | 型安全性 | OpenAPI + Codegen で対応可能 | GraphQL Codegen で自動生成 | | 運用負荷 | BFF ごとにコードが独立 | 統一されたスキーマ管理が必要 |
判断基準
-
BFF を選ぶべきケース
- クライアント種別が 2〜3 種類で、要件が明確に異なる
- チームに GraphQL の知見がない
- HTTP キャッシュを積極的に活用したい
- 段階的な移行を重視
-
GraphQL を選ぶべきケース
- クライアントが頻繁に必要なデータ形式を変更する
- モバイルアプリで通信量の最適化が最優先
- 統一されたスキーマ駆動開発を採用したい
3. BFF の設計パターン
パターン 1: 完全分離型
各 BFF が独立したリポジトリ・デプロイ単位を持つ構成。
リポジトリ:
- mobile-bff/
- web-bff/
- admin-bff/
- services/ (共通マイクロサービス)
メリット
- 完全に独立した開発サイクル
- 技術スタックを BFF ごとに選択可能
デメリット
- 共通ロジックの重複リスク
- インフラ管理の複雑化
パターン 2: モノレポ+共通モジュール
モノレポで BFF を管理し、共通処理をパッケージ化。
monorepo/
├── packages/
│ ├── auth-client/
│ ├── api-client/
│ └── logging/
└── apps/
├── mobile-bff/
├── web-bff/
└── admin-bff/
メリット
- 共通ロジックの再利用が容易
- 型定義の共有が簡単
デメリット
- ビルド時間の増加
- 依存関係の管理が複雑
パターン 3: Next.js Server Actions(Web BFF)
Next.js の Server Actions を Web BFF として活用。
// app/actions/products.ts
'use server'
import { getAuthUser } from '@/lib/auth'
import { productServiceClient } from '@/lib/api-clients'
export async function getProductsForWeb() {
const user = await getAuthUser()
// バックエンド API を呼び出し
const products = await productServiceClient.getProducts({
userId: user.id,
includeRecommendations: true,
})
// Web 向けに整形
return products.map(p => ({
id: p.id,
name: p.name,
price: p.price,
imageUrl: p.images[0]?.url,
// モバイルでは不要な SEO 用メタ情報
metaDescription: p.description,
structuredData: generateProductSchema(p),
}))
}
メリット
- Next.js の型システムがそのまま使える
- デプロイが容易(Vercel など)
デメリット
- Next.js に依存
- モバイル BFF には別途必要
4. BFF の実装例:Express + TypeScript
ディレクトリ構成
mobile-bff/
├── src/
│ ├── routes/
│ │ ├── products.ts
│ │ └── orders.ts
│ ├── services/
│ │ ├── productService.ts
│ │ └── orderService.ts
│ ├── middleware/
│ │ ├── auth.ts
│ │ └── errorHandler.ts
│ └── types/
│ └── api.ts
└── package.json
基本実装
// src/routes/products.ts
import { Router } from 'express'
import { authenticateUser } from '../middleware/auth'
import { ProductService } from '../services/productService'
import type { MobileProductResponse } from '../types/api'
const router = Router()
const productService = new ProductService()
// モバイル向け商品一覧
router.get('/products', authenticateUser, async (req, res) => {
const userId = req.user!.id
// 複数のマイクロサービスを統合
const [products, recommendations, inventory] = await Promise.all([
productService.getProducts(),
productService.getRecommendations(userId),
productService.getInventory(),
])
// モバイル向けに最適化したレスポンス
const response: MobileProductResponse = {
items: products.map(p => ({
id: p.id,
name: p.name,
// 画像は thumbnail のみ(通信量削減)
thumbnail: p.images.find(img => img.type === 'thumbnail')?.url,
price: p.price,
// 在庫ステータスを統合
inStock: inventory[p.id] > 0,
})),
// レコメンドは ID のみ
recommendedIds: recommendations.map(r => r.id),
}
res.json(response)
})
export default router
Service 層での API 統合
// src/services/productService.ts
import { apiClient } from '../lib/apiClient'
import type { Product } from '../types/domain'
export class ProductService {
async getProducts(): Promise<Product[]> {
// 商品マイクロサービスを呼び出し
const response = await apiClient.get('/api/products')
return response.data
}
async getRecommendations(userId: string) {
// レコメンドエンジンを呼び出し
const response = await apiClient.get(`/api/recommendations/${userId}`)
return response.data
}
async getInventory(): Promise<Record<string, number>> {
// 在庫管理システムを呼び出し
const response = await apiClient.get('/api/inventory')
return response.data.reduce((acc, item) => {
acc[item.productId] = item.quantity
return acc
}, {} as Record<string, number>)
}
}
5. 認証・認可の実装パターン
パターン 1: JWT をそのまま転送
BFF はトークンを検証せず、バックエンドに転送。
// src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express'
export function forwardAuth(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization
if (!token) {
return res.status(401).json({ error: 'Unauthorized' })
}
// バックエンド API クライアントにトークンを設定
req.apiClient = createApiClient({
headers: { Authorization: token }
})
next()
}
メリット: BFF の責務が軽い
デメリット: バックエンドへの不正リクエストを BFF で防げない
パターン 2: BFF でトークン検証
BFF で JWT を検証し、バックエンドには内部トークンを使用。
// src/middleware/auth.ts
import jwt from 'jsonwebtoken'
import { Request, Response, NextFunction } from 'express'
export function authenticateUser(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) {
return res.status(401).json({ error: 'Unauthorized' })
}
try {
// JWT を検証
const decoded = jwt.verify(token, process.env.JWT_PUBLIC_KEY!)
req.user = decoded as { id: string; role: string }
// バックエンド向けに内部サービストークンを使用
req.apiClient = createApiClient({
headers: { 'X-Internal-Token': process.env.SERVICE_TOKEN }
})
next()
} catch (err) {
return res.status(401).json({ error: 'Invalid token' })
}
}
メリット: 不正リクエストを BFF で遮断
デメリット: BFF で認証ロジックが重複
6. エラーハンドリングとロギング
統一的なエラーレスポンス
// src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express'
import { logger } from '../lib/logger'
export class ApiError extends Error {
constructor(
public statusCode: number,
public message: string,
public code?: string
) {
super(message)
}
}
export function errorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (err instanceof ApiError) {
// 意図したエラー
logger.warn({
message: err.message,
code: err.code,
path: req.path,
userId: req.user?.id,
})
return res.status(err.statusCode).json({
error: {
message: err.message,
code: err.code,
}
})
}
// 予期しないエラー
logger.error({
message: err.message,
stack: err.stack,
path: req.path,
userId: req.user?.id,
})
return res.status(500).json({
error: {
message: 'Internal server error',
code: 'INTERNAL_ERROR',
}
})
}
構造化ログの実装
// src/lib/logger.ts
import pino from 'pino'
export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: {
target: 'pino-pretty',
options: {
colorize: true,
ignore: 'pid,hostname',
translateTime: 'SYS:standard',
}
}
})
// リクエストログミドルウェア
export function requestLogger(req: Request, res: Response, next: NextFunction) {
const start = Date.now()
res.on('finish', () => {
logger.info({
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration: Date.now() - start,
userId: req.user?.id,
})
})
next()
}
7. パフォーマンス最適化
並列リクエストの実装
// 悪い例:直列処理
async function getProductDetail(productId: string) {
const product = await productService.getProduct(productId)
const reviews = await reviewService.getReviews(productId)
const related = await productService.getRelated(productId)
return { product, reviews, related }
}
// 良い例:並列処理
async function getProductDetail(productId: string) {
const [product, reviews, related] = await Promise.all([
productService.getProduct(productId),
reviewService.getReviews(productId),
productService.getRelated(productId),
])
return { product, reviews, related }
}
レスポンスキャッシュ
// src/middleware/cache.ts
import { createClient } from 'redis'
import { Request, Response, NextFunction } from 'express'
const redis = createClient({ url: process.env.REDIS_URL })
await redis.connect()
export function cacheMiddleware(ttl: number) {
return async (req: Request, res: Response, next: NextFunction) => {
const key = `cache:${req.path}:${JSON.stringify(req.query)}`
// キャッシュをチェック
const cached = await redis.get(key)
if (cached) {
return res.json(JSON.parse(cached))
}
// オリジナルの res.json を上書き
const originalJson = res.json.bind(res)
res.json = (body: any) => {
// キャッシュに保存
redis.setEx(key, ttl, JSON.stringify(body))
return originalJson(body)
}
next()
}
}
// 使用例
router.get('/products', cacheMiddleware(300), async (req, res) => {
// 5分間キャッシュされる
})
8. 運用・監視の実装
ヘルスチェック
// src/routes/health.ts
import { Router } from 'express'
import { apiClient } from '../lib/apiClient'
const router = Router()
router.get('/health', async (req, res) => {
const health = {
status: 'ok',
timestamp: new Date().toISOString(),
dependencies: {} as Record<string, string>,
}
// 依存サービスのヘルスチェック
try {
await apiClient.get('/health', { timeout: 1000 })
health.dependencies.productService = 'ok'
} catch (err) {
health.dependencies.productService = 'error'
health.status = 'degraded'
}
const statusCode = health.status === 'ok' ? 200 : 503
res.status(statusCode).json(health)
})
export default router
メトリクス収集
// src/lib/metrics.ts
import { Counter, Histogram, Registry } from 'prom-client'
const register = new Registry()
export const httpRequestDuration = new Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code'],
registers: [register],
})
export const httpRequestTotal = new Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status_code'],
registers: [register],
})
// メトリクス公開エンドポイント
router.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType)
res.end(await register.metrics())
})
9. デプロイとインフラ構成
Docker Compose での開発環境
# docker-compose.yml
version: '3.8'
services:
mobile-bff:
build: ./mobile-bff
ports:
- "3001:3000"
environment:
- NODE_ENV=development
- PRODUCT_SERVICE_URL=http://product-service:3000
- REDIS_URL=redis://redis:6379
depends_on:
- redis
- product-service
web-bff:
build: ./web-bff
ports:
- "3002:3000"
environment:
- NODE_ENV=development
- PRODUCT_SERVICE_URL=http://product-service:3000
depends_on:
- product-service
product-service:
build: ./services/product
ports:
- "4000:3000"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
Kubernetes デプロイ例
# k8s/mobile-bff-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mobile-bff
spec:
replicas: 3
selector:
matchLabels:
app: mobile-bff
template:
metadata:
labels:
app: mobile-bff
spec:
containers:
- name: mobile-bff
image: registry.example.com/mobile-bff:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
- name: PRODUCT_SERVICE_URL
valueFrom:
configMapKeyRef:
name: bff-config
key: product-service-url
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
10. 実務 Tips とトラブルシューティング
よくある問題と対処法
| 問題 | 原因 | 対処法 | |------|------|--------| | BFF のレスポンスが遅い | 複数サービスへの直列呼び出し | Promise.all で並列化 | | BFF で共通処理が重複 | 各 BFF で個別実装 | 共通ライブラリを npm パッケージ化 | | バックエンドの変更が BFF に波及 | 密結合 | API Gateway パターンで疎結合化 | | 認証トークンの期限切れ | リフレッシュトークン未対応 | BFF でリフレッシュ処理を実装 | | BFF が単一障害点に | 冗長化不足 | 複数インスタンス + ロードバランサー |
チーム体制の設計
- 小規模(〜5人): モノレポで全 BFF を管理
- 中規模(5〜15人): BFF ごとにオーナーを決め、共通基盤チームでライブラリ提供
- 大規模(15人〜): BFF を完全分離し、各チームが独立して開発
段階的な導入戦略
- フェーズ 1: 既存 API の前に Web BFF だけ配置
- フェーズ 2: モバイル BFF を追加し、通信量最適化
- フェーズ 3: バックエンドをマイクロサービス化し、BFF で統合
まとめ
BFF パターンは、複数のクライアント種別を持つアプリケーションで、それぞれに最適化された API を提供するための実用的なアーキテクチャです。
導入の判断基準
- クライアントが 2 種類以上あり、要件が明確に異なる
- モバイルで通信量の最適化が必要
- マイクロサービス化が進み、クライアント側での API 統合が複雑
実装のポイント
- 並列処理で複数サービスへのリクエストを最適化
- 共通処理はライブラリ化して重複を避ける
- キャッシュとロギングで運用性を高める
受託開発では、プロジェクトの規模とチーム体制に応じて、モノレポ構成から完全分離まで柔軟に選択できます。まずは Web BFF だけ導入し、効果を確認してから段階的に拡大するアプローチが現実的です。
Yureate では、BFF を含むバックエンドアーキテクチャ設計から実装・運用まで一貫して支援しています。既存システムへの BFF 導入や、マイクロサービス移行の相談もお気軽にお問い合わせください。
