Aufbau sauberer Architekturen in modernen Backend-Frameworks
Emily Parker
Product Engineer · Leapcell

Einleitung
In der sich ständig weiterentwickelnden Landschaft der Backend-Entwicklung ist die Wartung einer Codebasis, die nicht nur funktional, sondern auch skalierbar, testbar und leicht verständlich ist, von größter Bedeutung. Mit zunehmender Komplexität der Projekte weicht die anfängliche schnelle Entwicklung oft verschlungenen Abhängigkeiten, brüchigen Tests und hohen Änderungskosten. Diese Herausforderung spiegelt sich branchenübergreifend in verschiedenen Technologie-Stacks wider, was die Einführung robuster Architekturmuster zu einem kritischen Anliegen für Entwickler und Organisationen macht. Die von Robert C. Martin propagierten Prinzipien der Clean Architecture bieten eine überzeugende Lösung, indem sie die Trennung von Zuständigkeiten und die Unabhängigkeit von Frameworks, Datenbanken und Benutzeroberflächen betonen. Dieser Artikel befasst sich damit, wie diese leistungsstarken Prinzipien in drei namhaften Backend-Frameworks – Django, NestJS und FastAPI – praktisch angewendet werden können, um Entwicklern den Aufbau widerstandsfähigerer und wartbarerer Anwendungen zu ermöglichen.
Kernkonzepte verstehen
Bevor wir uns mit den Besonderheiten jedes Frameworks befassen, ist es wichtig, die grundlegenden Konzepte hinter der Clean Architecture zu verdeutlichen.
- Entitäten (Domänenschicht): Diese kapseln die Geschäftsregeln des gesamten Unternehmens. Sie sind die reinsten und qualitativ höchsten Richtlinien, unabhängig von anwendungsspezifischen Belangen. Änderungen in der Datenbank, der Benutzeroberfläche oder sogar externen Frameworks sollten Entitäten nicht beeinträchtigen.
- Use Cases (Anwendungsschicht): Diese Schicht enthält anwendungsspezifische Geschäftsregeln. Sie steuert den Datenfluss zu und von den Entitäten und legt fest, wie die Anwendung die Entitäten verwenden wird. Use Cases sollten nichts über die Datenbank oder das Web-Framework wissen. Schnittstellen (abstrakte Klassen) werden hier oft für externe Belange definiert.
- Interface Adapters (Infrastrukturschicht): Diese Schicht fungiert als Gateway zwischen den Use Cases und externen Belangen. Sie passt Daten von einem Format, das für die Use Cases und Entitäten praktisch ist, an ein Format an, das für Frameworks, Datenbanken oder externe Dienste geeignet ist, und umgekehrt. Beispiele hierfür sind Präsentatoren, Gateways und Controller.
- Frameworks & Treiber (Äußere Schicht): Diese äußerste Schicht besteht aus konkreten Implementierungen von Schnittstellen, die in den inneren Schichten definiert sind. Dazu gehören Web-Frameworks (Django, NestJS, FastAPI), Datenbanken (ORM-Implementierungen) und Integrationen externer APIs.
Das grundlegende Prinzip ist die Dependency Rule: Abhängigkeiten können nur nach innen zeigen. Innere Kreise dürfen niemals etwas über äußere Kreise wissen.
Implementierung von Clean Architecture
Lassen Sie uns untersuchen, wie sich diese Konzepte in praktischen Codebeispielen in unseren ausgewählten Frameworks wiederfinden.
Django
Django kann mit seiner "Batteries included"-Philosophie bei unvorsichtiger Vorgehensweise manchmal zu eng gekoppelten Anwendungen führen. Seine Modularität ermöglicht jedoch eine effektive Implementierung der Clean Architecture.
Beispiel Projektstruktur:
my_django_project/
├── core/ # Entitäten & Use Cases
│ ├── domain/ # Entitäten (reine Python-Objekte)
│ │ ├── models.py # z.B. User, Product Entitäten
│ │ └── __init__.py
│ ├── use_cases/ # Anwendungsfälle (Geschäftslogik)
│ │ ├── create_user.py
│ │ ├── get_product.py
│ │ └── __init__.py
│ ├── interfaces/ # Abstraktionen für externe Dienste
│ │ ├── user_repository.py # z.B. abstractUserRepository
│ │ └── __init__.py
└── application/ # Interface Adapters & Frameworks
├── users/
│ │ ├── adapters/ # Adapter für Datenbank/Web
│ │ │ ├── repositories.py # Konkrete ORM-Implementierungen (z.B. UserDjangoRepository)
│ │ │ └── controllers.py # Django Views/API Views, die Use Cases aufrufen
│ │ ├── urls.py
│ │ └── __init__.py
│ ├── products/
│ │ └── ...
│ ├── config/ # Django Projekt-Einstellungen
│ ├── manage.py
│ └── ...
Codebeispiel (Django):
-
core/domain/models.py
(Entität):# Reines Python-Objekt, keine Django ORM-Vererbung 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): # Geschäftsregel if "@" not in new_email: raise ValueError("Ungültiges E-Mail-Format") self.email = new_email
-
core/interfaces/user_repository.py
(Abstrakter Repository):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
(Use Case):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) # Möglicherweise weitere Geschäftslogik vor dem Speichern self.user_repository.save(user) return user
-
application/users/adapters/repositories.py
(Konkreter Django ORM Repository):from django.db import models from core.domain.models import User as DomainUser from core.interfaces.user_repository import UserRepository # Django ORM Modell (Infrastruktur-Detail) 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 View/Controller):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': 'Benutzername und E-Mail sind erforderlich'}, 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)
Diese Konfiguration stellt sicher, dass die Kern-Geschäftslogik (die Entität User
und der Use Case CreateUser
) vollständig von Djangos ORM oder HTTP-Spezifika unberührt bleibt.
NestJS
NestJS ist stark von Angular und Spring inspiriert und fördert einen modularen und strukturierten Ansatz durch die Verwendung von Modulen, Controllern, Diensten und Providern. Dies passt von Natur aus gut zur Clean Architecture.
Beispiel Projektstruktur:
src/
├── core/ # Entitäten & Use Cases
│ ├── domain/ # Entitäten (Schnittstellen/Klassen)
│ │ ├── user.ts
│ │ └── product.ts
│ ├── use-cases/ # Anwendungsbezogene Geschäftslogik
│ │ ├── create-user.use-case.ts
│ │ ├── get-product.use-case.ts
│ │ └── interfaces/ # Abstrakte Repositories/Gateways hier definiert
│ │ ├── user-repository.interface.ts
│ │ └── product-repository.interface.ts
└── infrastructure/ # Interface Adapters & Frameworks
├── user/
│ │ ├── adapters/ # Konkrete Implementierungen für Repositories und Controller
│ │ │ ├── user.controller.ts # Verarbeitet HTTP-Anfragen, injiziert Use Cases
│ │ │ ├── user.entity.ts # TypeORM-Entität
│ │ │ └── user.repository.ts # Konkrete TypeORM-Repository-Implementierung
│ │ ├── user.module.ts
│ │ └── dtos/
│ │ └── create-user.dto.ts
│ ├── product/
│ │ └── ...
│ ├── main.ts
│ └── app.module.ts
Codebeispiel (NestJS):
-
src/core/domain/user.ts
(Entität):// Reine TypeScript-Klasse/Schnittstelle, Framework-unabhängig 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('Ungültiges E-Mail-Format'); } this.email = newEmail; } }
-
src/core/use-cases/interfaces/user-repository.interface.ts
(Abstraktes Repository):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'; // Token für Dependency Injection
-
src/core/use-cases/create-user.use-case.ts
(Use Case):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); // Zusätzliche Geschäftslogik await this.userRepository.save(user); return user; } }
-
src/infrastructure/user/adapters/user.entity.ts
(TypeORM-Entität - Infrastruktur-Detail):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
(Konkretes TypeORM-Repository):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); // Domain zu ORM-Entität zuordnen await this.ormRepository.save(ormUser); } }
-
src/infrastructure/user/adapters/user.controller.ts
(NestJS-Controller):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'; // oder @nestjs/common/response für Nest's Abstraktion 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: 'Ein unerwarteter Fehler ist aufgetreten' }); } } }
Das robuste Dependency-Injection-System von NestJS erleichtert die Injektion des abstrakten Repositories in den Use Case und anschließend des Use Cases in den Controller, wodurch die Abhängigkeitsregel eingehalten wird.
FastAPI
FastAPI, bekannt für seine Leistung und modernen Python-Funktionen wie Typ-Hints und Async/Await, kann ebenfalls elegant mit Clean Architecture strukturiert werden, indem es sein Dependency-Injection-System nutzt.
Beispiel Projektstruktur:
my_fastapi_project/
├── core/ # Entitäten & Use Cases
│ ├── domain/ # Entitäten (Pydantic-Modelle oder reine Python-Klassen)
│ │ ├── user.py
│ │ └── product.py
│ ├── use_cases/ # Anwendungsbezogene Geschäftslogik
│ │ ├── create_user.py
│ │ ├── get_product.py
│ │ └── interfaces/ # Abstrakte Repositories/Gateways
│ │ ├── user_repository.py
│ │ └── product_repository.py
└── infrastructure/
├── api/ # FastAPI-Routen-Definitionen
│ ├── v1/
│ │ ├── endpoints/
│ │ │ ├── user.py # Controller (FastAPI Endpunkte)
│ │ │ └── product.py
│ │ └── routers.py
├── persistence/ # Datenbank-Implementierungen
│ ├── repositories/
│ │ ├── user_sqlalchemy_repo.py
│ │ └── product_sqlalchemy_repo.py
│ ├── database.py # DB-Verbindungs-Setup
│ └── models.py # SQLAlchemy ORM-Modelle
├── main.py # FastAPI App-Initialisierung
└── dependencies.py # Dependency-Injection-Setup
Codebeispiel (FastAPI):
-
core/domain/user.py
(Entität):from pydantic import BaseModel # Kann Pydantic für Validierung verwenden, aber die Kernlogik ist getrennt class User(BaseModel): id: str username: str email: str def update_email(self, new_email: str): if "@" not in new_email: raise ValueError("Ungültiges E-Mail-Format") self.email = new_email
-
core/use_cases/interfaces/user_repository.py
(Abstraktes Repository):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
(Use Case):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-Modell - Infrastruktur-Detail):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
(Konkretes SQLAlchemy Repository):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 Dependency Injection Helfer):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 Endpunkt/Controller):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))
Die Depends
-Funktion von FastAPI ist ein unglaublich leistungsfähiges Werkzeug für die Injektion von Abhängigkeiten. Sie ermöglicht eine saubere Trennung zwischen der HTTP-Schicht und der Kernlogik der Anwendung.
Fazit
Die Übernahme der Clean Architecture in Django, NestJS oder FastAPI bietet eine robuste Blaupause für den Aufbau wartbarer, testbarer und skalierbarer Backend-Anwendungen. Durch strikte Einhaltung der Dependency Rule und Trennung von Zuständigkeiten in verschiedene Schichten können Entwickler Systeme erreichen, die unabhängig von externen Frameworks und Datenbanken sind, was eine leichtere Weiterentwicklung und Anpassung im Laufe der Zeit ermöglicht. Dieser Ansatz stellt sicher, dass die Kern-Geschäftslogik makellos und unberührt von technologischen Veränderungen bleibt, und befähigt Anwendungen so, die Zeit zu überdauern.
Letztendlich ist Clean Architecture nicht nur ein Muster, sondern eine Denkweise, die langfristige Qualität und architektonische Integrität über kurzfristige Zweckmäßigkeit stellt.