Die Architektur von NestJS folgt bewährten Software-Design-Prinzipien und kombiniert moderne JavaScript/TypeScript-Konzepte mit etablierten architektonischen Patterns. Das Framework ist so konzipiert, dass es skalierbare, wartbare und testbare Anwendungen ermöglicht, die von kleinen APIs bis hin zu komplexen Enterprise-Systemen reichen.
NestJS implementiert eine modulare, schichtenbasierte Architektur, die stark von Angular inspiriert ist. Das Framework folgt dem Prinzip der “Convention over Configuration” und bietet dabei genügend Flexibilität für individuelle Anpassungen.
Kernarchitektur-Komponenten bilden das Fundament jeder NestJS-Anwendung:
Die Application Instance steht an der Spitze der
Hierarchie und orchestriert alle anderen Komponenten. Sie wird über die
NestFactory.create()-Methode erstellt und fungiert als
Einstiegspunkt für die gesamte Anwendung. Die Application Instance
verwaltet den Dependency Injection Container, registriert Module und
startet den HTTP-Server.
Module organisieren die Anwendung in logische Einheiten. Jedes Modul kapselt verwandte Funktionalitäten und definiert, welche Provider, Controller und andere Module verfügbar sind. Das Root Module bildet den Startpunkt, von dem aus alle anderen Module geladen werden.
Provider sind die grundlegenden Bausteine des Dependency Injection-Systems. Sie können Services, Repositories, Factories oder jede andere Klasse sein, die injectable ist. Provider werden im DI-Container registriert und können in andere Komponenten injiziert werden.
Controller handhaben eingehende HTTP-Requests und geben Responses zurück. Sie definieren die API-Endpunkte der Anwendung und delegieren Business Logic an Services. Controller sind dünn gehalten und fokussieren sich auf Request/Response-Handling.
Middleware, Guards, Interceptors und Pipes bilden die Verarbeitungsschicht zwischen eingehenden Requests und Controllern. Sie ermöglichen Cross-Cutting Concerns wie Authentifizierung, Validierung, Logging und Transformation.
Execution Context stellt Metadaten über den aktuellen Request zur Verfügung und ermöglicht es verschiedenen Komponenten, kontextabhängig zu agieren. Der Execution Context ist besonders wichtig für Guards und Interceptors.
Request Lifecycle in NestJS folgt einem vorhersagbaren Muster:
Diese Pipeline ist hochgradig konfigurierbar und ermöglicht es, an jedem Punkt der Request-Verarbeitung einzugreifen.
NestJS fördert eine klare Schichtentrennung, die Verantwortlichkeiten sauber voneinander abgrenzt und die Wartbarkeit der Anwendung erhöht.
Die Presentation Layer ist die äußerste Schicht der Anwendung und verantwortlich für die Kommunikation mit Clients. Diese Schicht umfasst:
HTTP Controller handhaben REST API-Endpunkte und
sind mit Dekoratoren wie @Controller(),
@Get(), @Post() annotiert. Sie empfangen
HTTP-Requests, extrahieren relevante Daten und delegieren die
Verarbeitung an die Business Logic Schicht.
WebSocket Gateways ermöglichen
Real-Time-Kommunikation über WebSockets. Sie folgen einem ähnlichen
Pattern wie HTTP Controller, nutzen aber WebSocket-spezifische
Dekoratoren wie @WebSocketGateway() und
@SubscribeMessage().
GraphQL Resolvers handhaben GraphQL-Queries und
-Mutations. Sie sind mit @Resolver() annotiert und
definieren die GraphQL-Schema-Implementierung.
Microservice Controllers ermöglichen die Kommunikation in Microservice-Architekturen über verschiedene Transport-Layer wie TCP, Redis oder RabbitMQ.
Die Presentation Layer sollte dünn gehalten werden und sich auf Request/Response-Handling konzentrieren. Business Logic gehört nicht in diese Schicht.
Die Business Logic Layer enthält die Kernlogik der Anwendung und implementiert Geschäftsregeln und -prozesse:
Domain Services implementieren geschäftsspezifische
Operationen und Regeln. Sie sind mit @Injectable()
annotiert und können andere Services als Dependencies haben. Domain
Services sollten framework-agnostisch sein und die reine Geschäftslogik
enthalten.
Application Services orchestrieren komplexe Geschäftsprozesse und koordinieren mehrere Domain Services. Sie implementieren Use Cases und Application-spezifische Workflows.
Utility Services stellen wiederverwendbare Hilfsfunktionen zur Verfügung, die von verschiedenen Teilen der Anwendung genutzt werden können.
External Service Clients kapseln die Kommunikation mit externen APIs und Services. Sie abstrahieren die Details der externen Integration und stellen eine saubere Schnittstelle für die eigene Anwendung zur Verfügung.
Die Business Logic Layer ist unabhängig von HTTP-Details und kann theoretisch in verschiedenen Kontexten (Web, CLI, Tests) verwendet werden.
Die Data Access Layer abstrahiert den Zugriff auf persistente Daten und externe Systeme:
Repositories implementieren das Repository Pattern und kapseln Datenzugriff-spezifische Logik. Sie können direkt mit ORMs wie TypeORM oder Prisma arbeiten oder custom Implementierungen verwenden.
Data Access Objects (DAOs) bieten eine Low-Level-Schnittstelle für Datenbankoperationen. Sie sind besonders nützlich für komplexe Queries oder datenbankspezifische Optimierungen.
ORM Entities definieren die Datenmodelle und Mapping zwischen Objekten und Datenbankstrukturen. Sie sind mit ORM-spezifischen Dekoratoren annotiert.
Cache Providers abstrahieren Caching-Mechanismen und können In-Memory-Caches oder externe Cache-Systeme wie Redis kapseln.
Die Data Access Layer isoliert die oberen Schichten von Datenbank-spezifischen Details und ermöglicht es, Datenquellen zu ändern, ohne die Business Logic zu beeinträchtigen.
Bestimmte Aspekte durchdringen alle Schichten und werden als Cross-Cutting Concerns behandelt:
Logging wird über alle Schichten hinweg implementiert und nutzt NestJS’s integriertes Logger-System oder externe Logging-Bibliotheken.
Error Handling wird durch Exception Filters und globale Error Handler implementiert, die Fehler aus allen Schichten abfangen und entsprechend behandeln.
Security umfasst Authentication, Authorization, Input Validation und andere sicherheitsrelevante Aspekte, die durch Guards, Pipes und Middleware implementiert werden.
Monitoring und Metrics werden durch Interceptors und spezielle Provider implementiert, die Performance-Daten sammeln und an Monitoring-Systeme weiterleiten.
Das Modulsystem ist das Herzstück der NestJS-Architektur und ermöglicht die Organisation komplexer Anwendungen in überschaubare, wiederverwendbare Einheiten.
Feature Modules kapseln spezifische Geschäftsfunktionalitäten wie User Management, Order Processing oder Payment Handling. Jedes Feature Module enthält alle Komponenten, die für diese Funktionalität benötigt werden: Controller, Services, Repositories und DTOs.
Ein typisches Feature Module könnte folgendermaßen strukturiert sein:
users/
├── controllers/
│ └── users.controller.ts
├── services/
│ └── users.service.ts
├── repositories/
│ └── users.repository.ts
├── dto/
│ ├── create-user.dto.ts
│ └── update-user.dto.ts
├── entities/
│ └── user.entity.ts
└── users.module.ts
Shared Modules enthalten wiederverwendbare Komponenten, die von mehreren Feature Modules genutzt werden. Beispiele sind Database-Module, HTTP-Client-Module oder Common Utility-Services.
Core Modules enthalten singleton Services, die Application-weit verfügbar sein sollen. Typische Beispiele sind Configuration Services, Logger oder Authentication Services. Core Modules werden normalerweise nur einmal im Root Module importiert.
Common Modules exportieren häufig verwendete Funktionalitäten wie Validators, Transformers oder Base Classes, die in verschiedenen Feature Modules benötigt werden.
Dynamic Modules ermöglichen die Konfiguration von Modulen zur Laufzeit. Sie sind besonders nützlich für wiederverwendbare Bibliotheks-Module, die verschiedene Konfigurationen unterstützen müssen.
Ein Dynamic Module wird durch eine statische Methode erstellt, die
ein DynamicModule-Objekt zurückgibt:
@Module({})
export class DatabaseModule {
static forRoot(options: DatabaseOptions): DynamicModule {
return {
module: DatabaseModule,
providers: [
{
provide: 'DATABASE_OPTIONS',
useValue: options,
},
DatabaseService,
],
exports: [DatabaseService],
global: true,
};
}
}Async Dynamic Modules ermöglichen es, Module basierend auf asynchronen Operationen zu konfigurieren, wie dem Laden von Konfigurationsdateien oder dem Verbinden zu externen Services.
Module können andere Module importieren, um deren exportierte Provider zu nutzen. Dies schafft ein Dependency-Graph, der von NestJS zur Laufzeit aufgelöst wird.
Circular Dependencies zwischen Modulen werden von
NestJS erkannt und können durch Forward References oder Refactoring
aufgelöst werden. Das Framework bietet forwardRef() für
Fälle, in denen zirkuläre Abhängigkeiten unvermeidlich sind.
Module Scoping bestimmt, welche Provider in welchem
Scope verfügbar sind. Module können Provider als global
markieren, wodurch sie Application-weit verfügbar werden, ohne explizit
importiert werden zu müssen.
Die modulare Architektur erleichtert das Testen erheblich. Einzelne Module können isoliert getestet werden, indem Mock-Implementierungen für Dependencies bereitgestellt werden.
Das Test.createTestingModule()-API ermöglicht es,
temporäre Module für Tests zu erstellen und spezifische Provider zu
überschreiben oder zu mocken.
NestJS bietet umfassende Unterstützung für Microservices-Architekturen und ermöglicht es, von monolithischen Anwendungen zu verteilten Systemen zu migrieren, ohne das grundlegende Programming Model zu ändern.
Request-Response Pattern ermöglicht synchrone Kommunikation zwischen Services. Der Client sendet eine Anfrage und wartet auf eine Antwort. Dieses Pattern eignet sich für Operationen, die eine sofortige Antwort erfordern.
Event-based Pattern nutzt asynchrone Messaging für lose gekoppelte Kommunikation. Services emittieren Events, auf die andere Services reagieren können, ohne direkte Abhängigkeiten zu haben.
Message-based Pattern verwendet Message Queues für zuverlässige asynchrone Kommunikation. Messages werden persistent gespeichert und können auch bei temporären Service-Ausfällen zugestellt werden.
NestJS unterstützt verschiedene Transport-Mechanismen für Microservice-Kommunikation:
TCP Transport bietet einfache, direkte Kommunikation zwischen Services. Er ist ideal für interne Netzwerke mit geringer Latenz und hoher Bandbreite.
Redis Transport nutzt Redis als Message Broker und bietet Pub/Sub-Funktionalitäten. Redis ist besonders geeignet für Event-driven Architekturen und bietet gute Performance.
NATS Transport ist ein leichtgewichtiger, high-performance Messaging-System, das für Cloud-native Anwendungen optimiert ist. NATS unterstützt verschiedene Messaging-Patterns und bietet Clustering-Funktionalitäten.
RabbitMQ Transport ist ein vollwertiger Message Broker mit erweiterten Features wie Routing, Persistence und High Availability. RabbitMQ eignet sich für komplexe Messaging-Szenarien.
gRPC Transport ermöglicht typisierte, hochperformante Kommunikation zwischen Services. gRPC nutzt Protocol Buffers für Serialisierung und HTTP/2 für Transport.
In Microservice-Architekturen müssen Services sich gegenseitig finden und Traffic effizient verteilen können:
Service Registry hält Informationen über verfügbare Service-Instanzen und deren Endpoints. Services registrieren sich beim Start und deregistrieren sich beim Herunterfahren.
Client-side Load Balancing verteilt Requests auf verfügbare Service-Instanzen. NestJS kann mit Service Discovery-Systemen wie Consul oder Eureka integriert werden.
Circuit Breaker Pattern verhindert Cascade-Failures in verteilten Systemen. Wenn ein Service nicht verfügbar ist, wird der Circuit Breaker geöffnet und weitere Calls werden verhindert, bis der Service wieder verfügbar ist.
Ein API Gateway fungiert als einziger Einstiegspunkt für externe Clients und orchestriert Calls zu verschiedenen Microservices:
Request Routing leitet eingehende Requests an die entsprechenden Microservices weiter, basierend auf URL-Pfaden oder anderen Kriterien.
Authentication und Authorization werden zentral im Gateway gehandhabt, anstatt in jedem einzelnen Microservice implementiert zu werden.
Rate Limiting, Caching und Monitoring können ebenfalls zentral im Gateway implementiert werden, wodurch diese Cross-Cutting Concerns aus den Microservices ausgelagert werden.
Response Aggregation ermöglicht es, Daten aus mehreren Microservices zu kombinieren und eine einheitliche Response an den Client zu senden.
Microservice-Architekturen erfordern umfassende Monitoring- und Observability-Strategien:
Distributed Tracing verfolgt Requests durch mehrere Services und ermöglicht es, Performance-Bottlenecks und Fehler zu identifizieren. NestJS kann mit Tracing-Systemen wie Jaeger oder Zipkin integriert werden.
Centralized Logging sammelt Logs von allen Services an einem zentralen Ort. Structured Logging mit Correlation IDs ermöglicht es, zusammengehörige Log-Einträge zu identifizieren.
Metrics und Health Checks überwachen die Gesundheit und Performance von Services. NestJS bietet integrierte Health Check-Mechanismen und kann mit Monitoring-Systemen wie Prometheus integriert werden.
Error Tracking sammelt und aggregiert Fehler aus allen Services. Dies ermöglicht es, Probleme schnell zu identifizieren und zu beheben.
NestJS ermöglicht graduelle Migration von monolithischen zu Microservice-Architekturen:
Strangler Fig Pattern ersetzt schrittweise Teile der monolithischen Anwendung durch Microservices. Neue Funktionalitäten werden als Microservices implementiert, während bestehende Funktionalitäten nach und nach migriert werden.
Database per Service isoliert Daten pro Service und verhindert direkte Datenbankzugriffe zwischen Services. Dies erfordert sorgfältige Planung von Data Consistency-Strategien.
Shared Libraries ermöglichen die Wiederverwendung von gemeinsamen Code-Teilen zwischen Services, während gleichzeitig die Service-Autonomie gewährleistet wird.
Die modulare Architektur von NestJS macht diese Migration besonders elegant, da Module relativ einfach in separate Services extrahiert werden können, ohne das grundlegende Programming Model zu ändern.