🦁

【超初心者向け】Firebaseのフォルダ構成どうすればいい?

2025/03/03に公開

こんにちは、ワニかず@40歳 出戻りエンジニアです。

FirebaseでAPIの構築ができるまで設定が済んだ状態となり、
さぁ、コーディングをしようにも、何から手を付けていいかわからないですよね。(私のこと)

ということで、今回は、
Firebaseのようなバックエンドシステムの基本的な設計パターン
レイヤードアーキテクチャについてまとめました。

レイヤードアーキテクチャ

Firebaseのようなバックエンドシステムでは、

  • APIレイヤー、ビジネスロジック層、データアクセス層に分けるこのような設計パターンは一般的に「レイヤードアーキテクチャ」(Layered Architecture)または「3層アーキテクチャ」(Three-tier Architecture)と呼ばれています。

各層の責務を明確に分離し、依存関係を制御することで、保守性と拡張性の高いシステムを実現することを目的としています。

各層の主な役割は:

  1. API層(Presentation Layer)

    • HTTPリクエストのハンドリング
    • バリデーション
    • レスポンスの整形
  2. ビジネスロジック層(Business/Service Layer)

    • アプリケーションのコアとなるビジネスロジック
    • トランザクション管理
    • 複数のデータアクセス層の調整
  3. データアクセス層(Data Access Layer)

    • データベースとのやり取り
    • データの永続化
    • クエリの実行

この構造はFirebaseに限らず、多くのバックエンドアプリケーションで採用されている一般的なアーキテクチャパターンです。

Firebaseでの一般的な構造

src/
├── routes/          # ルーティング定義
├── controllers/     # リクエストハンドリング
├── services/        # ビジネスロジック
├── models/          # データモデル定義
└── repositories/    # Firebaseとのデータアクセス

各レイヤーの詳細:

  1. ルーティング層(routes/)
// routes/userRoutes.ts
export const userRoutes = functions.https.onRequest((req, res) => {
  if (req.method === 'GET') {
    return UserController.getUser(req, res);
  }
});
  1. コントローラー層(controllers/)
// controllers/userController.ts
export class UserController {
  static async getUser(req: Request, res: Response) {
    try {
      const userId = req.params.id;
      const user = await UserService.getUser(userId);
      res.json(user);
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  }
}
  1. サービス層(services/)
// services/userService.ts
export class UserService {
  static async getUser(userId: string) {
    const user = await UserRepository.findById(userId);
    // ビジネスロジックの処理
    return user;
  }
}
  1. モデル層(models/)
// models/user.ts
export interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}
  1. リポジトリ層(repositories/)
// repositories/userRepository.ts
export class UserRepository {
  static async findById(userId: string): Promise<User> {
    const doc = await admin.firestore()
      .collection('users')
      .doc(userId)
      .get();
    return doc.data() as User;
  }
}

主なポイント:

  1. 依存の方向

    • 上位層から下位層への一方向の依存
    • 下位層は上位層を知らない設計
  2. エラーハンドリング

    • 各層で適切なエラー処理
    • コントローラー層でのグローバルエラーハンドリング
  3. 型定義

    • インターフェースやDTOを使用した明確な型定義
    • 層間のデータ受け渡しの型安全性確保
  4. テスト容易性

    • 各層が独立しているため、単体テストが書きやすい
    • モックやスタブの使用が容易

このような構造により、コードの保守性、拡張性、テスト容易性が向上し、大規模なアプリケーションの開発・運用が容易になります。

Discussion