モダンバックエンドフレームワークにおけるクリーンアーキテクチャの構築
Emily Parker
Product Engineer · Leapcell

はじめに
進化し続けるバックエンド開発の世界では、機能的であるだけでなく、スケーラブルで、テスト可能で、理解しやすいコードベースを維持することが極めて重要です。プロジェクトが複雑化するにつれて、当初の迅速な開発は、絡み合った依存関係、脆いテスト、そして変更コストの高さへと姿を変えていきます。この課題は様々な技術スタックに深く共鳴しており、堅牢なアーキテクチャパターンの採用は、開発者と組織双方にとって重要な関心事となっています。ロバート・C・マーティンが提唱するクリーンアーキテクチャの原則は、懸念事項の分離とフレームワーク、データベース、UIからの独立を強調することで、説得力のある解決策を提供します。この記事では、これらの強力な原則が、3つの著名なバックエンドフレームワーク、Django、NestJS、FastAPIでどのように実践的に適用され、開発者がより回復力があり、保守性の高いアプリケーションを構築できるようにするかを探ります。
コアコンセプトの理解
各フレームワークの詳細に入る前に、クリーンアーキテクチャの背後にある基本的な概念を明確にすることが不可欠です。
- エンティティ(ドメインレイヤー): これらは、企業全体のビジネスルールをカプセル化します。これらは最も純粋で最高レベルのポリシーであり、アプリケーション固有の懸念事項から独立しています。データベース、UI、あるいは外部フレームワークの変更がエンティティに影響を与えるべきではありません。
- ユースケース(アプリケーションレイヤー): このレイヤーには、アプリケーション固有のビジネスルールが含まれます。エンティティとの間でデータの流れを調整し、アプリケーションがエンティティをどのように使用するかを指示します。ユースケースは、データベースやWebフレームワークについて知るべきではありません。インターフェース(抽象クラス)は、外部の懸念事項のためにここで定義されることがよくあります。
- インターフェースアダプター(インフラストラクチャレイヤー): このレイヤーは、ユースケースと外部の懸念事項との間のゲートウェイとして機能します。ユースケースとエンティティに便利な形式のデータを、フレームワーク、データベース、または外部サービスに適した形式に、またその逆も同様に変換します。例としては、プレゼンター、ゲートウェイ、コントローラーなどがあります。
- フレームワークとドライバー(外部レイヤー): この最も外側のレイヤーは、内側のレイヤーで定義されたインターフェースの具体的な実装で構成されます。これには、Webフレームワーク(Django、NestJS、FastAPI)、データベース(ORM実装)、および外部API統合が含まれます。
ここでの基本的な原則は依存関係のルールです:依存関係は内側に向かってのみ指すことができます。内側の円は、外側の円について何も知ってはなりません。
クリーンアーキテクチャの実装
これらの概念が、選択したフレームワーク全体で実践的なコード例にどのように変換されるかを見てみましょう。
Django
Djangoは"Include the batteries"(すぐに使える機能満載)という哲学を持っていますが、注意深くアプローチしないと、緊密に結合したアプリケーションにつながることがあります。しかし、そのモジュール性により、クリーンアーキテクチャの効果的な実装が可能になります。
プロジェクト構造の例:
my_django_project/
├── core/ # エンティティ & ユースケース
│ ├── domain/ # エンティティ(純粋なPythonオブジェクト)
│ │ ├── models.py # 例:User, Productエンティティ
│ │ └── __init__.py
│ ├── use_cases/ # ユースケース(ビジネスロジック)
│ │ ├── create_user.py
│ │ ├── get_product.py
│ │ └── __init__.py
│ ├── interfaces/ # 外部サービスのための抽象化
│ │ ├── user_repository.py # 例:abstractUserRepository
│ │ └── __init__.py
└── application/ # インターフェースアダプター & フレームワーク
├── users/
│ ├── adapters/
│ │ ├── repositories.py # 具体的なORM実装(例:UserDjangoRepository)
│ │ └── controllers.py # ユースケースを呼び出すDjangoビュー/APIビュー
│ ├── urls.py
│ └── __init__.py
├── products/
│ └── ...
├── config/ # Djangoプロジェクト設定
├── manage.py
└── ...
コード例(Django):
-
core/domain/models.py
(エンティティ):# Django ORM継承なしの純粋なPythonオブジェクト class User: def __init__(self, id: str, username: str, email: str): self.id = id self.username = username self.email = email def update_email(self, new_email: str): # ビジネスルール if "@" not in new_email: raise ValueError("Invalid email format") self.email = new_email
-
core/interfaces/user_repository.py
(抽象リポジトリ):from abc import ABC, abstractmethod from core.domain.models import User class UserRepository(ABC): @abstractmethod def get_by_id(self, user_id: str) -> User: pass @abstractmethod def save(self, user: User): pass
-
core/use_cases/create_user.py
(ユースケース):from core.domain.models import User from core.interfaces.user_repository import UserRepository class CreateUser: def __init__(self, user_repository: UserRepository): self.user_repository = user_repository def execute(self, user_id: str, username: str, email: str) -> User: user = User(id=user_id, username=username, email=email) # 保存前の追加ビジネスロジックの可能性 self.user_repository.save(user) return user
-
application/users/adapters/repositories.py
(具体的なDjango ORMリポジトリ):from django.db import models from core.domain.models import User as DomainUser from core.interfaces.user_repository import UserRepository # Django ORMモデル(インフラストラクチャの詳細) class UserORM(models.Model): id = models.CharField(max_length=255, primary_key=True) username = models.CharField(max_length=255) email = models.EmailField() class Meta: db_table = 'users' class DjangoUserRepository(UserRepository): def get_by_id(self, user_id: str) -> DomainUser: orm_user = UserORM.objects.get(id=user_id) return DomainUser(id=orm_user.id, username=orm_user.username, email=orm_user.email) def save(self, user: DomainUser): UserORM.objects.update_or_create( id=user.id, defaults={'username': user.username, 'email': user.email} )
-
application/users/adapters/controllers.py
(Djangoビュー/コントローラー):from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from core.use_cases.create_user import CreateUser from application.users.adapters.repositories import DjangoUserRepository import uuid class CreateUserAPIView(APIView): def post(self, request): user_id = str(uuid.uuid4()) username = request.data.get('username') email = request.data.get('email') if not username or not email: return Response({'error': 'Username and email are required'}, status=status.HTTP_400_BAD_REQUEST) repo = DjangoUserRepository() create_user_use_case = CreateUser(user_repository=repo) try: user = create_user_use_case.execute(user_id=user_id, username=username, email=email) return Response({'id': user.id, 'username': user.username, 'email': user.email}, status=status.HTTP_201_CREATED) except ValueError as e: return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
このセットアップにより、コアビジネスロジック(User
エンティティとCreateUser
ユースケース)は、DjangoのORMやHTTPの固有の詳細を完全に認識しないままになります。
NestJS
NestJSはAngularとSpringに強く影響を受けており、モジュール、コントローラー、サービス、プロバイダーの使用を通じて、モジュラーで構造化されたアプローチを促進します。これは、クリーンアーキテクチャとうまく整合します。
プロジェクト構造の例:
src/
├── core/ # エンティティ & ユースケース
│ ├── domain/ # エンティティ(インターフェース/クラス)
│ │ ├── user.ts
│ │ └── product.ts
│ ├── use-cases/ # アプリケーション固有のビジネスロジック
│ │ ├── create-user.use-case.ts
│ │ ├── get-product.use-case.ts
│ │ └── interfaces/ # ここで定義された抽象リポジトリ/ゲートウェイ
│ │ ├── user-repository.interface.ts
│ │ └── product-repository.interface.ts
└── infrastructure/ # インターフェースアダプター & フレームワーク
├── user/
│ ├── adapters/
│ │ ├── user.controller.ts # HTTPリクエストを処理し、ユースケースを注入する
│ │ ├── user.entity.ts # TypeORMエンティティ
│ │ └── user.repository.ts # 具体的なTypeORMリポジトリ実装
│ ├── user.module.ts
│ └── dtos/
│ └── create-user.dto.ts
├── product/
│ └── ...
├── main.ts
└── app.module.ts
コード例(NestJS):
-
src/core/domain/user.ts
(エンティティ):// フレームワークに依存しない、純粋なTypeScriptクラス/インターフェース export interface User { id: string; username: string; email: string; updateEmail(newEmail: string): void; } export class UserEntity implements User { constructor(public id: string, public username: string, public email: string) {} updateEmail(newEmail: string): void { if (!newEmail.includes('@')) { throw new Error('Invalid email format'); } this.email = newEmail; } }
-
src/core/use-cases/interfaces/user-repository.interface.ts
(抽象リポジトリ):import { User } from '../domain/user'; export interface IUserRepository { getById(id: string): Promise<User | null>; save(user: User): Promise<void>; } export const USER_REPOSITORY = 'USER_REPOSITORY'; // 依存関係注入のためのトークン
-
src/core/use-cases/create-user.use-case.ts
(ユースケース):import { Inject, Injectable } from '@nestjs/common'; import { User, UserEntity } from '../domain/user'; import { IUserRepository, USER_REPOSITORY } from './interfaces/user-repository.interface'; @Injectable() export class CreateUserUseCase { constructor( @Inject(USER_REPOSITORY) private readonly userRepository: IUserRepository, ) {} async execute(id: string, username: string, email: string): Promise<User> { const user = new UserEntity(id, username, email); // 追加のビジネスロジック await this.userRepository.save(user); return user; } }
-
src/infrastructure/user/adapters/user.entity.ts
(TypeORMエンティティ - インフラストラクチャの詳細):import { Entity, PrimaryColumn, Column } from 'typeorm'; @Entity('users') export class UserTypeORMEntity { @PrimaryColumn() id: string; @Column() username: string; @Column() email: string; }
-
src/infrastructure/user/adapters/user.repository.ts
(具体的なTypeORMリポジトリ):import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { IUserRepository } from '../../../core/use-cases/interfaces/user-repository.interface'; import { User, UserEntity } from '../../../core/domain/user'; import { UserTypeORMEntity } from './user.entity'; import { Injectable } from '@nestjs/common'; @Injectable() export class UserTypeORMRepository implements IUserRepository { constructor( @InjectRepository(UserTypeORMEntity) private readonly ormRepository: Repository<UserTypeORMEntity>, ) {} async getById(id: string): Promise<User | null> { const ormUser = await this.ormRepository.findOne({ where: { id } }); return ormUser ? new UserEntity(ormUser.id, ormUser.username, ormUser.email) : null; } async save(user: User): Promise<void> { const ormUser = this.ormRepository.create(user); // ドメインをORMエンティティにマッピング await this.ormRepository.save(ormUser); } }
-
src/infrastructure/user/adapters/user.controller.ts
(NestJSコントローラー):import { Body, Controller, Post, Res, HttpStatus } from '@nestjs/common'; import { CreateUserUseCase } from '../../../core/use-cases/create-user.use-case'; import { CreateUserDto } from '../dtos/create-user.dto'; import { Response } from 'express'; // またはNestの抽象化のための@nestjs/common/response import { v4 as uuidv4 } from 'uuid'; @Controller('users') export class UsersController { constructor(private readonly createUserUseCase: CreateUserUseCase) {} @Post() async createUser(@Body() createUserDto: CreateUserDto, @Res() res: Response) { try { const userId = uuidv4(); const user = await this.createUserUseCase.execute(userId, createUserDto.username, createUserDto.email); return res.status(HttpStatus.CREATED).json({ id: user.id, username: user.username, email: user.email }); } catch (error) { if (error instanceof Error) { return res.status(HttpStatus.BAD_REQUEST).json({ message: error.message }); } return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ message: 'An unexpected error occurred' }); } } }
NestJSの堅牢な依存関係注入システムにより、抽象リポジトリをユースケースに、そしてユースケースをコントローラーに容易に注入でき、依存関係のルールを維持できます。
FastAPI
FastAPIは、そのパフォーマンスと型ヒントやasync/awaitのようなモダンなPython機能で知られており、クリーンアーキテクチャをエレガントに構造化できます。その依存関係注入システムを活用できます。
プロジェクト構造の例:
my_fastapi_project/
├── core/ # エンティティ & ユースケース
│ ├── domain/ # エンティティ(PydanticモデルまたはプレーンPythonクラス)
│ │ ├── user.py
│ │ └── product.py
│ ├── use_cases/ # アプリケーション固有のビジネスロジック
│ │ ├── create_user.py
│ │ ├── get_product.py
│ │ └── interfaces/ # 抽象リポジトリ/ゲートウェイ
│ │ ├── user_repository.py
│ │ └── product_repository.py
└── infrastructure/
├── api/ # FastAPIルート定義
│ ├── v1/
│ │ ├── endpoints/
│ │ │ ├── user.py # コントローラー(FastAPIエンドポイント)
│ │ │ └── product.py
│ │ └── routers.py
├── persistence/ # データベース実装
│ ├── repositories/
│ │ ├── user_sqlalchemy_repo.py
│ │ └── product_sqlalchemy_repo.py
│ ├── database.py # DB接続設定
│ └── models.py # SQLAlchemy ORMモデル
├── main.py # FastAPIアプリ初期化
└── dependencies.py # 依存関係注入設定
コード例(FastAPI):
-
core/domain/user.py
(エンティティ):from pydantic import BaseModel # Pydanticを検証のために使用できるが、コアロジックは分離されている class User(BaseModel): id: str username: str email: str def update_email(self, new_email: str): if "@" not in new_email: raise ValueError("Invalid email format") self.email = new_email
-
core/use_cases/interfaces/user_repository.py
(抽象リポジトリ):from abc import ABC, abstractmethod from typing import Optional from core.domain.user import User class UserRepository(ABC): @abstractmethod async def get_by_id(self, user_id: str) -> Optional[User]: pass @abstractmethod async def save(self, user: User) -> None: pass
-
core/use_cases/create_user.py
(ユースケース):from core.domain.user import User from core.use_cases.interfaces.user_repository import UserRepository class CreateUser: def __init__(self, user_repository: UserRepository): self.user_repository = user_repository async def execute(self, user_id: str, username: str, email: str) -> User: user = User(id=user_id, username=username, email=email) await self.user_repository.save(user) return user
-
infrastructure/persistence/models.py
(SQLAlchemy ORMモデル - インフラストラクチャの詳細):from sqlalchemy import Column, String from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class UserORM(Base): __tablename__ = "users" id = Column(String, primary_key=True, index=True) username = Column(String, unique=True, index=True) email = Column(String, unique=True, index=True)
-
infrastructure/persistence/repositories/user_sqlalchemy_repo.py
(具体的なSQLAlchemyリポジトリ):from sqlalchemy.orm import Session from core.domain.user import User as DomainUser from core.use_cases.interfaces.user_repository import UserRepository from infrastructure.persistence.models import UserORM from typing import Optional class SQLAlchemyUserRepository(UserRepository): def __init__(self, db: Session): self.db = db async def get_by_id(self, user_id: str) -> Optional[DomainUser]: orm_user = self.db.query(UserORM).filter(UserORM.id == user_id).first() return DomainUser.model_validate(orm_user) if orm_user else None async def save(self, user: DomainUser) -> None: orm_user = self.db.query(UserORM).filter(UserORM.id == user.id).first() if orm_user: for key, value in user.model_dump().items(): setattr(orm_user, key, value) else: orm_user = UserORM(**user.model_dump()) self.db.add(orm_user) self.db.commit() self.db.refresh(orm_user)
-
infrastructure/dependencies.py
(FastAPI依存関係注入ヘルパー):from sqlalchemy.orm import Session from infrastructure.persistence.database import get_db from core.use_cases.interfaces.user_repository import UserRepository from infrastructure.persistence.repositories.user_sqlalchemy_repo import SQLAlchemyUserRepository from core.use_cases.create_user import CreateUser def get_user_repository(db: Session = Depends(get_db)) -> UserRepository: return SQLAlchemyUserRepository(db) def get_create_user_use_case(user_repo: UserRepository = Depends(get_user_repository)) -> CreateUser: return CreateUser(user_repository=user_repo)
-
infrastructure/api/v1/endpoints/user.py
(FastAPIエンドポイント/コントローラー):from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel import uuid from core.domain.user import User as DomainUser from core.use_cases.create_user import CreateUser from infrastructure.dependencies import get_create_user_use_case router = APIRouter() class CreateUserRequest(BaseModel): username: str email: str class CreateUserResponse(BaseModel): id: str username: str email: str @router.post("/users", response_model=CreateUserResponse, status_code=status.HTTP_201_CREATED) async def create_user_endpoint( request: CreateUserRequest, create_user_uc: CreateUser = Depends(get_create_user_use_case) ): try: user_id = str(uuid.uuid4()) user = await create_user_uc.execute(user_id=user_id, username=request.username, email=request.email) return CreateUserResponse.model_validate(user) except ValueError as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
FastAPIのDepends
機能は、依存関係の注入に非常に強力であり、HTTPレイヤーとアプリケーションのコアロジックとの間のクリーンな分離を可能にします。
結論
Django、NestJS、またはFastAPIでクリーンアーキテクチャを採用することは、保守性、テスト可能性、スケーラビリティの高いバックエンドアプリケーションを構築するための堅牢な設計図を提供します。依存関係のルールを厳密に遵守し、懸念事項を明確に分離することで、外部フレームワークやデータベースから独立したシステムを実現し、進化や順応を容易にします。このアプローチにより、コアビジネスロジックは技術的な変化に影響されずに維持され、アプリケーションは時代の試練に耐えられるようになります。
最終的に、クリーンアーキテクチャは単なるパターンではなく、短期的な利便性よりも長期的な品質とアーキテクチャの整合性を重視する考え方です。