Closed6

100日チャレンジ day14 (BFF Dashboard)

riddle_tecriddle_tec

昨日
https://zenn.dev/gin_nazo/scraps/4012fd77036c2e


https://blog.framinal.life/entry/2025/04/14/154104

100日チャレンジに感化されたので、アレンジして自分でもやってみます。

やりたいこと

  • 世の中のさまざまなドメインの簡易実装をつくり、バックエンドの実装に慣れる(dbスキーマ設計や、関数の分割、使いやすいインターフェイスの切り方に慣れる
  • 設計力(これはシステムのオーバービューを先に自分で作ってaiに依頼できるようにする
  • 生成aiをつかったバイブコーティングになれる
  • 実際にやったことはzennのスクラップにまとめ、成果はzennのブログにまとめる(アプリ自体の公開は必須ではないかコードはgithubにおく)

できたもの

https://github.com/lirlia/100day_challenge_backend/tree/main/day14_bff_dashboard

riddle_tecriddle_tec

Day 14: パーソナライズド・ダッシュボード (BFF パターン実装)

1. 概要

このアプリケーションは、Backend for Frontend (BFF) パターンの理解と実践を目的とした、パーソナライズド・ダッシュボードです。
ユーザーごとに最適化された情報を表示するために、フロントエンド (ダッシュボード UI) は専用の BFF エンドポイントのみと通信します。BFF は内部的に複数の独立した「コア API」を呼び出し、取得したデータをフロントエンドの表示に適した形に加工・集約して返します。

2. 機能要件

2.1. コア API 群 (Core APIs)

以下のダミー API を Next.js Route Handlers として実装します。データは Prisma + SQLite で管理します (シードデータで初期投入)。

  • /api/core/profile/[userId] (GET):
    • 指定された userId のユーザープロフィール情報 (名前、メールアドレス、簡単な自己紹介文など) を返します。
    • 存在しない userId の場合は 404 を返します。
  • /api/core/activities/[userId] (GET):
    • 指定された userId の最近のアクティビティ履歴 (例: 操作内容、日時) のリストを返します。最新 5 件程度で十分です。
    • 該当ユーザーのアクティビティがない場合は空配列を返します。
  • /api/core/notifications (GET):
    • 全ユーザー共通のお知らせ情報 (タイトル、本文、公開日時) のリストを返します。最新 3 件程度で十分です。
  • /api/core/recommendations/[userId] (GET):
    • 指定された userId に基づくおすすめアイテム (商品名、画像 URL、簡単な説明) のリストを返します。ユーザーごとに異なる結果を返すようにシミュレートします (例: userId の偶奇で内容を変える)。

2.2. BFF (Backend for Frontend) API

ダッシュボード表示専用の BFF エンドポイントを Next.js Route Handler として実装します。

  • /api/bff/dashboard (GET):
    • クエリパラメータ userId を受け取ります。
    • 内部で以下の処理を行います:
      1. /api/core/profile/[userId] を呼び出し、プロフィール情報を取得します。
      2. /api/core/activities/[userId] を呼び出し、アクティビティ履歴を取得します。
      3. /api/core/notifications を呼び出し、お知らせ情報を取得します。
      4. /api/core/recommendations/[userId] を呼び出し、おすすめ情報を取得します。
      • これらの API 呼び出しは並行して実行することが望ましいです (Promise.all)。
    • 各コア API から取得したデータを、フロントエンドが扱いやすい単一の JSON オブジェクトに整形して返します。
      • 例: { profile: {...}, activities: [...], notifications: [...], recommendations: [...] }
    • いずれかのコア API 呼び出しに失敗した場合でも、取得できたデータだけでレスポンスを返すか、あるいは BFF としてエラーを返すか検討します (今回は取得できたデータだけで返す方針とします)。
    • userId が指定されない場合や、必須のコア API (例: profile) がエラーを返した場合は 400 や 500 エラーを返します。

2.3. フロントエンド (UI)

  • ユーザー選択:
    • アプリケーション上部などに、操作対象のユーザーを選択するためのシンプルなドロップダウンリストを設置します (例: User 1, User 2)。
    • 選択された userId はクライアント側で状態として保持します。
  • ダッシュボード表示 (/):
    • ユーザーが選択されると、選択された userId をクエリパラメータとして /api/bff/dashboard を呼び出します。
    • BFF から返却されたデータを元に、以下の情報をカード形式などで表示します:
      • ユーザープロフィール
      • 最近のアクティビティ一覧
      • お知らせ一覧
      • おすすめアイテム一覧
    • データのロード中やエラー発生時には適切なフィードバックを表示します。

3. 非機能要件

  • フレームワーク: Next.js (App Router)
  • 言語: TypeScript
  • データベース: SQLite
  • ORM: Prisma
  • スタイリング: Tailwind CSS
  • API 通信: Fetch API
  • 状態管理: React Hooks (useState, useEffect)
  • エラーハンドリング: 基本的な try-catch, console.error
  • テスト: 手動確認 (UI 操作、API レスポンス確認)

4. データモデル (Prisma スキーマ案)

コア API が利用するデータを管理します。

// prisma/schema.prisma
model User {
  id          Int      @id @default(autoincrement())
  name        String
  email       String   @unique
  bio         String?
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  activities  Activity[]
  // recommendations は userId の偶奇などで動的に生成するためDBモデル不要
}

model Activity {
  id          Int      @id @default(autoincrement())
  userId      Int
  user        User     @relation(fields: [userId], references: [id])
  action      String   // 例: "記事 A を読んだ", "商品 B を購入した"
  timestamp   DateTime @default(now())
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  @@index([userId, timestamp])
}

model Notification {
  id          Int      @id @default(autoincrement())
  title       String
  content     String
  publishedAt DateTime @default(now())
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  @@index([publishedAt])
}

model Recommendation {
  id          Int      @id @default(autoincrement())
  itemName    String
  imageUrl    String
  description String?
  targetUserType String // "even" or "odd" (ユーザーIDの偶奇で出し分け用)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
}

5. API エンドポイント定義

5.1. コア API

  • GET /api/core/profile/[userId]
    • 成功 (200): { "id": 1, "name": "User One", "email": "user1@example.com", "bio": "..." }
    • 失敗 (404): { "error": "User not found" }
  • GET /api/core/activities/[userId]
    • 成功 (200): [ { "id": 1, "action": "記事 A を読んだ", "timestamp": "..." }, ... ]
  • GET /api/core/notifications
    • 成功 (200): [ { "id": 1, "title": "お知らせ1", "content": "...", "publishedAt": "..." }, ... ]
  • GET /api/core/recommendations/[userId]
    • 成功 (200): [ { "id": 1, "itemName": "おすすめ商品A", "imageUrl": "...", "description": "..." }, ... ]

5.2. BFF API

  • GET /api/bff/dashboard?userId=<userId>
    • 成功 (200):
      {
        "profile": { "id": 1, "name": "User One", ... },
        "activities": [ { "id": 1, "action": "...", ... }, ... ],
        "notifications": [ { "id": 1, "title": "...", ... }, ... ],
        "recommendations": [ { "id": 1, "itemName": "...", ... }, ... ]
      }
      
    • 一部失敗時 (例: activities のみ失敗) (200):
      {
        "profile": { "id": 1, "name": "User One", ... },
        "activities": null, // または空配列、エラー情報など
        "notifications": [ { "id": 1, "title": "...", ... }, ... ],
        "recommendations": [ { "id": 1, "itemName": "...", ... }, ... ]
      }
      
    • 失敗 (400): { "error": "userId is required" }
    • 失敗 (500): { "error": "Failed to fetch mandatory data (e.g., profile)" }

6. 画面構成案

  • ルートページ (/)
    • ヘッダー: ユーザー選択ドロップダウン
    • メインエリア (ダッシュボード):
      • 左上: プロフィールカード
      • 左下: おすすめアイテムカード (横スクロール可)
      • 右上: お知らせリスト
      • 右下: 最近のアクティビティリスト

7. ディレクトリ構成案

/day14_bff_dashboard
├── app/
│   ├── api/
│   │   ├── bff/
│   │   │   └── dashboard/
│   │   │       └── route.ts         # BFF API: GET /api/bff/dashboard
│   │   └── core/
│   │       ├── profile/
│   │       │   └── [userId]/
│   │       │       └── route.ts     # Core API: GET /api/core/profile/[userId]
│   │       ├── activities/
│   │       │   └── [userId]/
│   │       │       └── route.ts     # Core API: GET /api/core/activities/[userId]
│   │       ├── notifications/
│   │       │   └── route.ts         # Core API: GET /api/core/notifications
│   │       └── recommendations/
│   │           └── [userId]/
│   │               └── route.ts     # Core API: GET /api/core/recommendations/[userId]
│   ├── (pages)/
│   │   └── page.tsx                 # ダッシュボード UI
│   ├── _lib/                        # BFF ロジックやコア API ロジックの共通部分など
│   ├── layout.tsx
│   └── globals.css
├── components/                      # 共有 UI コンポーネント (例: UserSelector, DashboardCard)
│   └── ui/
├── lib/                             # PrismaClient, 共通ユーティリティ
│   └── db.ts
├── prisma/
│   ├── schema.prisma
│   ├── migrations/
│   └── dev.db
├── public/
├── .env
├── next.config.mjs
├── package.json
├── tsconfig.json
└── README.md                        # この仕様書を記述

8. 実装手順案

  1. プロジェクト初期化: template -> day14_bff_dashboard, package.json 更新, README.md 更新。
  2. Prisma 設定: schema.prisma 定義, npx prisma migrate deploy, npx prisma db seed で初期データ投入 (User, Activity, Notification, Recommendation)。
  3. コア API 実装:/api/core/.../route.ts を実装。Prisma を使ってデータを取得し、JSON で返す。
  4. BFF API 実装: /api/bff/dashboard/route.ts を実装。内部でコア API を fetch で並行呼び出し、結果を整形して返す。エラーハンドリングも考慮。
  5. UI コンポーネント実装:
    • UserSelector: ユーザー選択ドロップダウン。
    • DashboardCard: 各セクション (Profile, Activity 等) を表示する汎用カードコンポーネント。
  6. フロントエンド実装: /app/page.tsx を実装。
    • UserSelector を配置し、ユーザー選択状態を管理。
    • 選択された userId/api/bff/dashboard を呼び出す処理を実装 (useEffect)。
    • 取得したデータを DashboardCard を使って表示。
    • ローディング状態、エラー状態の表示。
  7. 動作確認: UI 操作、各 API のレスポンス確認。

9. シーケンス図 (ダッシュボード表示フロー)

riddle_tecriddle_tec

承知いたしました。以下に「パーソナライズド・ダッシュボード」アプリケーション作成の作業計画を立てます。

作業計画:

  1. プロジェクト初期化:

    • template ディレクトリをコピーして day14_bff_dashboard ディレクトリを作成します。
    • day14_bff_dashboard ディレクトリに移動します。
    • package.jsonname フィールドを day14_bff_dashboard に更新します。
    • README.md の内容を先ほど作成した仕様書で上書きします。
  2. データベース設定と初期データ投入:

    • prisma/schema.prisma を仕様書に合わせて更新します (User, Activity, Notification, Recommendation モデル定義)。
    • npx prisma migrate deploy を実行してデータベーススキーマを適用します。
    • prisma/seed.ts を作成し、各モデルに初期データ (ユーザー2名分程度、関連データ含む) を投入するスクリプトを記述します。
    • (必要であれば package.json の seed スクリプトを ts-node を使うように修正し、ts-node を dev dependency としてインストールします)。
    • npx prisma db seed を実行して初期データを投入します。
    • (任意) npx prisma studio でデータ投入を確認します。
  3. コア API 実装:

    • /api/core/profile/[userId]/route.ts を実装します (GET)。
    • /api/core/activities/[userId]/route.ts を実装します (GET)。
    • /api/core/notifications/route.ts を実装します (GET)。
    • /api/core/recommendations/[userId]/route.ts を実装します (GET、userId の偶奇で結果を分岐)。
    • curl コマンド等で各コア API の動作を確認します。
  4. BFF API 実装:

    • /api/bff/dashboard/route.ts を実装します (GET)。
    • 内部で fetch を用いて4つのコア API を並行 (Promise.all) で呼び出します。
    • 取得したデータを整形し、単一の JSON レスポンスとして返します。
    • コア API の一部失敗時のハンドリングや、必須パラメータ (userId) のチェックを行います。
    • curl コマンド等で BFF API の動作を確認します (異なる userId で結果が変わることも確認)。
  5. UI コンポーネント実装:

    • components/UserSelector.tsx: ユーザー選択用ドロップダウン。
    • components/DashboardCard.tsx: 各セクション表示用の汎用カード。
    • (任意) 必要に応じて components/ui/ に Shadcn/UI などのベースコンポーネントを導入します (例: Card, Select)。
  6. フロントエンドページ実装:

    • app/page.tsx をクライアントコンポーネント ('use client') として実装します。
    • UserSelector で選択されたユーザー ID を状態管理します。
    • 選択されたユーザー ID に基づき、/api/bff/dashboardfetch で呼び出します。
    • ローディング状態、エラー状態をハンドリングします。
    • 取得したデータを DashboardCard を用いて画面に表示します (プロフィール、アクティビティ、お知らせ、おすすめ)。
    • Tailwind CSS でレイアウトを整えます。
  7. 最終動作確認:

    • ブラウザで http://localhost:3001 を表示し、ユーザー切り替え、データ表示、ローディング/エラー表示などを一通り確認します。
    • (任意) Playwright などの E2E テストツールで基本的な動作を確認します。

この計画に基づき、まずは ステップ 1: プロジェクト初期化 から開始します。

riddle_tecriddle_tec

今回はシンプルなので特になし
gemini2.5-exp が全然使い物にならなかったので一度ディレクトリ丸ごと消した


BFF ってフロントエンドの複雑さを切り出したものなんだな。ただこれって誰が管理するの?がめちゃくちゃ揉めそうだな。。。あとapi 変えた時に bff もフロントもやらないといけないと考えると結構面倒。

このスクラップは4ヶ月前にクローズされました