Middleware, Interceptors, Guards, Pipes und Exception Filters bilden die Request/Response-Pipeline von NestJS und ermöglichen die elegante Behandlung von Cross-Cutting Concerns. Diese Komponenten arbeiten in einer definierten Reihenfolge zusammen und bieten granulare Kontrolle über jeden Aspekt der HTTP-Request-Verarbeitung. Das Verständnis dieser Pipeline ist entscheidend für die Entwicklung robuster, sicherer und wartbarer NestJS-Anwendungen.
Middleware sind Funktionen, die in der Request/Response-Pipeline vor den Route-Handlern ausgeführt werden. Sie haben Zugriff auf Request- und Response-Objekte und können diese modifizieren, die Pipeline unterbrechen oder zur nächsten Middleware weiterleiten. NestJS Middleware basiert auf Express.js Middleware und ist daher mit dem Express-Ecosystem kompatibel.
Middleware in NestJS erfüllt verschiedene wichtige Funktionen in der Request-Verarbeitung:
Request-Preprocessing: Middleware kann eingehende Requests analysieren, modifizieren oder anreichern, bevor sie die Route-Handler erreichen. Dies umfasst Aufgaben wie Request-Parsing, Header-Manipulation oder Datenextrahierung.
Authentication und Authorization: Middleware kann Benutzer authentifizieren, Sessions verwalten und Zugriffskontrollen implementieren, bevor sensible Route-Handler ausgeführt werden.
Logging und Monitoring: Middleware eignet sich ideal für Request-Logging, Performance-Monitoring und Metrics-Sammlung, da sie jeden Request zentral erfassen kann.
Cross-Origin Resource Sharing (CORS): CORS-Header können durch Middleware gesetzt werden, um Browser-basierte Anfragen von verschiedenen Domains zu ermöglichen.
Request Validation: Grundlegende Request-Validierung und Preprocessing können in Middleware implementiert werden, bevor spezialisiertere Validation Pipes angewendet werden.
Middleware wird in einer definierten Reihenfolge ausgeführt, die die Request/Response-Pipeline bestimmt:
Incoming Request
↓
1. Global Middleware (app.use())
↓
2. Module-bound Middleware (MiddlewareConsumer)
↓
3. Guards (@UseGuards())
↓
4. Interceptors (pre-controller)
↓
5. Pipes (@UsePipes())
↓
6. Controller Handler
↓
7. Interceptors (post-controller)
↓
8. Exception Filters
↓
Response
Funktionale Middleware sind einfache Funktionen, die der Express.js-Middleware-Signatur folgen:
// src/common/middleware/logger.middleware.ts
import { Request, Response, NextFunction } from 'express';
export function LoggerMiddleware(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
const { method, originalUrl, ip } = req;
const userAgent = req.get('User-Agent') || '';
console.log(`[${new Date().toISOString()}] ${method} ${originalUrl} - ${ip} - ${userAgent}`);
// Response-Event-Listener für Completion-Logging
res.on('finish', () => {
const duration = Date.now() - start;
const { statusCode } = res;
console.log(`[${new Date().toISOString()}] ${method} ${originalUrl} - ${statusCode} - ${duration}ms`);
});
next();
}
// Erweiterte funktionale Middleware mit Konfiguration
export function createCorsMiddleware(options: {
origin?: string | string[];
credentials?: boolean;
optionsSuccessStatus?: number;
}) {
return (req: Request, res: Response, next: NextFunction) => {
const origin = req.headers.origin;
// Origin-Prüfung
if (options.origin) {
const allowedOrigins = Array.isArray(options.origin) ? options.origin : [options.origin];
if (origin && allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
} else {
res.setHeader('Access-Control-Allow-Origin', '*');
}
// Standard CORS-Headers
res.setHeader('Access-Control-Allow-Methods', 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
if (options.credentials) {
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
// Preflight-Requests handhaben
if (req.method === 'OPTIONS') {
res.status(options.optionsSuccessStatus || 204).send();
return;
}
next();
};
}
// Rate-Limiting-Middleware
export function createRateLimitMiddleware(options: {
windowMs: number;
maxRequests: number;
message?: string;
}) {
const requests = new Map<string, { count: number; resetTime: number }>();
return (req: Request, res: Response, next: NextFunction) => {
const clientId = req.ip || 'unknown';
const now = Date.now();
const windowStart = now - options.windowMs;
// Alte Einträge bereinigen
for (const [ip, data] of requests.entries()) {
if (data.resetTime < windowStart) {
requests.delete(ip);
}
}
const clientData = requests.get(clientId) || { count: 0, resetTime: now + options.windowMs };
if (clientData.resetTime < now) {
// Window ist abgelaufen, zurücksetzen
clientData.count = 1;
clientData.resetTime = now + options.windowMs;
} else {
clientData.count++;
}
requests.set(clientId, clientData);
// Rate-Limit-Headers setzen
res.setHeader('X-RateLimit-Limit', options.maxRequests);
res.setHeader('X-RateLimit-Remaining', Math.max(0, options.maxRequests - clientData.count));
res.setHeader('X-RateLimit-Reset', Math.ceil(clientData.resetTime / 1000));
if (clientData.count > options.maxRequests) {
res.status(429).json({
error: 'Too Many Requests',
message: options.message || 'Rate limit exceeded',
retryAfter: Math.ceil((clientData.resetTime - now) / 1000),
});
return;
}
next();
};
}Klassenbasierte Middleware bieten mehr Struktur und ermöglichen Dependency Injection:
// src/common/middleware/request-id.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
// Request-ID generieren oder aus Header extrahieren
const requestId = req.headers['x-request-id'] as string || uuidv4();
// Request-ID zu Request und Response hinzufügen
req.headers['x-request-id'] = requestId;
res.setHeader('X-Request-ID', requestId);
// Request-ID für spätere Verwendung verfügbar machen
(req as any).requestId = requestId;
next();
}
}
// src/common/middleware/security.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class SecurityMiddleware implements NestMiddleware {
constructor(private configService: ConfigService) {}
use(req: Request, res: Response, next: NextFunction) {
// Security Headers setzen
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// HSTS nur in Production
if (this.configService.get('NODE_ENV') === 'production') {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}
// Content Security Policy
const csp = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self'",
"frame-ancestors 'none'",
].join('; ');
res.setHeader('Content-Security-Policy', csp);
next();
}
}
// src/common/middleware/auth-middleware.ts
import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { JwtService } from '@nestjs/jwt';
import { UserService } from '../../users/services/user.service';
@Injectable()
export class AuthMiddleware implements NestMiddleware {
constructor(
private jwtService: JwtService,
private userService: UserService,
) {}
async use(req: Request, res: Response, next: NextFunction) {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedException('Missing or invalid authorization header');
}
const token = authHeader.substring(7);
const payload = this.jwtService.verify(token);
// User aus Datenbank laden
const user = await this.userService.findOne(payload.sub);
if (!user || !user.isActive) {
throw new UnauthorizedException('User not found or inactive');
}
// User zu Request hinzufügen
(req as any).user = user;
next();
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
}
}
// src/common/middleware/api-version.middleware.ts
import { Injectable, NestMiddleware, BadRequestException } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class ApiVersionMiddleware implements NestMiddleware {
private readonly supportedVersions = ['1.0', '1.1', '2.0'];
private readonly defaultVersion = '2.0';
use(req: Request, res: Response, next: NextFunction) {
// Version aus verschiedenen Quellen extrahieren
let version = this.extractVersion(req);
if (!version) {
version = this.defaultVersion;
}
if (!this.supportedVersions.includes(version)) {
throw new BadRequestException(`Unsupported API version: ${version}. Supported versions: ${this.supportedVersions.join(', ')}`);
}
// Version zu Request hinzufügen
(req as any).apiVersion = version;
res.setHeader('X-API-Version', version);
next();
}
private extractVersion(req: Request): string | null {
// 1. Aus Accept-Header (application/json;version=1.0)
const acceptHeader = req.headers.accept;
if (acceptHeader) {
const versionMatch = acceptHeader.match(/version=([0-9.]+)/);
if (versionMatch) {
return versionMatch[1];
}
}
// 2. Aus Custom Header
const versionHeader = req.headers['x-api-version'] as string;
if (versionHeader) {
return versionHeader;
}
// 3. Aus Query Parameter
const versionQuery = req.query.version as string;
if (versionQuery) {
return versionQuery;
}
return null;
}
}Middleware wird in NestJS auf verschiedene Weise konfiguriert, abhängig vom Scope und den Anforderungen:
// main.ts - Globale funktionale Middleware
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggerMiddleware, createRateLimitMiddleware } from './common/middleware';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Globale funktionale Middleware
app.use(LoggerMiddleware);
app.use(createRateLimitMiddleware({
windowMs: 15 * 60 * 1000, // 15 Minuten
maxRequests: 100,
message: 'Too many requests from this IP, please try again later.',
}));
// Built-in Middleware
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
await app.listen(3000);
}
bootstrap();// src/app.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { RequestMethod } from '@nestjs/common';
import { RequestIdMiddleware } from './common/middleware/request-id.middleware';
import { SecurityMiddleware } from './common/middleware/security.middleware';
import { AuthMiddleware } from './common/middleware/auth.middleware';
import { ApiVersionMiddleware } from './common/middleware/api-version.middleware';
@Module({
imports: [
// Module imports
],
controllers: [],
providers: [],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
// Middleware für alle Routen
consumer
.apply(RequestIdMiddleware, SecurityMiddleware)
.forRoutes('*');
// API-Versionierung nur für API-Routen
consumer
.apply(ApiVersionMiddleware)
.forRoutes({ path: 'api/*', method: RequestMethod.ALL });
// Auth-Middleware nur für geschützte Routen
consumer
.apply(AuthMiddleware)
.exclude(
{ path: 'auth/login', method: RequestMethod.POST },
{ path: 'auth/register', method: RequestMethod.POST },
{ path: 'health', method: RequestMethod.GET },
{ path: 'docs', method: RequestMethod.GET },
)
.forRoutes(
{ path: 'api/*', method: RequestMethod.ALL },
{ path: 'admin/*', method: RequestMethod.ALL },
);
// Spezifische Middleware für bestimmte Controller
consumer
.apply(AdminAuthMiddleware)
.forRoutes(AdminController);
// Middleware für spezifische HTTP-Methoden
consumer
.apply(WriteOperationMiddleware)
.forRoutes(
{ path: '*', method: RequestMethod.POST },
{ path: '*', method: RequestMethod.PUT },
{ path: '*', method: RequestMethod.PATCH },
{ path: '*', method: RequestMethod.DELETE },
);
}
}// src/users/users.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UserValidationMiddleware } from './middleware/user-validation.middleware';
import { UserAuditMiddleware } from './middleware/user-audit.middleware';
@Module({
controllers: [UsersController],
providers: [],
})
export class UsersModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
// Validierung vor User-Operationen
consumer
.apply(UserValidationMiddleware)
.forRoutes(
{ path: 'users', method: RequestMethod.POST },
{ path: 'users/:id', method: RequestMethod.PUT },
{ path: 'users/:id', method: RequestMethod.PATCH },
);
// Audit-Logging für alle User-Operationen
consumer
.apply(UserAuditMiddleware)
.forRoutes(UsersController);
}
}Interceptors sind eine mächtige Funktionalität von NestJS, die es ermöglicht, die Request/Response-Pipeline vor und nach der Ausführung von Route-Handlern zu manipulieren. Sie basieren auf dem Aspect-Oriented Programming (AOP) Konzept und nutzen RxJS Observables für asynchrone Stream-Manipulation.
Interceptors implementieren das
NestInterceptor-Interface und haben Zugriff auf den
ExecutionContext und CallHandler. Sie
können:
Request-Transformation: Eingehende Requests modifizieren oder anreichern, bevor sie den Controller erreichen.
Response-Transformation: Ausgehende Responses transformieren, filtern oder anreichern.
Exception-Handling: Exceptions abfangen und in custom Responses umwandeln.
Timing und Monitoring: Execution-Zeit messen und Performance-Metriken sammeln.
Caching: Response-Caching implementieren und Cache-Strategien verwalten.
Logging: Detailliertes Request/Response-Logging mit Context-Informationen.
Der ExecutionContext bietet Informationen über den
aktuellen Ausführungskontext:
// src/common/interceptors/context-info.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class ContextInfoInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// Context-Typ bestimmen
const contextType = context.getType();
console.log(`Context Type: ${contextType}`); // 'http', 'ws', 'rpc'
if (contextType === 'http') {
// HTTP-Context analysieren
const httpContext = context.switchToHttp();
const request = httpContext.getRequest();
const response = httpContext.getResponse();
console.log(`HTTP Method: ${request.method}`);
console.log(`URL: ${request.url}`);
console.log(`User-Agent: ${request.headers['user-agent']}`);
}
// Handler-Informationen
const handler = context.getHandler();
const controller = context.getClass();
console.log(`Controller: ${controller.name}`);
console.log(`Handler: ${handler.name}`);
// Arguments (falls verfügbar)
const args = context.getArgs();
console.log(`Arguments count: ${args.length}`);
return next.handle().pipe(
tap(data => {
console.log(`Response data:`, data);
}),
);
}
}
// src/common/interceptors/metadata.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
@Injectable()
export class MetadataInterceptor implements NestInterceptor {
constructor(private reflector: Reflector) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// Metadaten aus Dekoratoren lesen
const roles = this.reflector.get<string[]>('roles', context.getHandler());
const isPublic = this.reflector.get<boolean>('isPublic', context.getHandler());
const version = this.reflector.get<string>('version', context.getClass());
console.log('Metadata:', { roles, isPublic, version });
// Metadaten-basierte Logik
if (isPublic) {
console.log('Public endpoint - no special handling needed');
}
if (roles && roles.length > 0) {
console.log(`Endpoint requires roles: ${roles.join(', ')}`);
}
return next.handle();
}
}Der CallHandler stellt die handle()-Methode
bereit, die den Route-Handler ausführt und ein Observable
zurückgibt:
// src/common/interceptors/timing.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class TimingInterceptor implements NestInterceptor {
private readonly logger = new Logger(TimingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const startTime = Date.now();
const request = context.switchToHttp().getRequest();
const { method, url } = request;
this.logger.log(`Starting ${method} ${url}`);
return next.handle().pipe(
tap({
next: (data) => {
const duration = Date.now() - startTime;
this.logger.log(`Completed ${method} ${url} in ${duration}ms`);
},
error: (error) => {
const duration = Date.now() - startTime;
this.logger.error(`Failed ${method} ${url} in ${duration}ms - ${error.message}`);
},
}),
);
}
}
// src/common/interceptors/response-transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface StandardResponse<T> {
success: boolean;
data: T;
timestamp: string;
path: string;
method: string;
statusCode: number;
}
@Injectable()
export class ResponseTransformInterceptor<T> implements NestInterceptor<T, StandardResponse<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<StandardResponse<T>> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
return next.handle().pipe(
map(data => ({
success: true,
data,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
statusCode: response.statusCode,
})),
);
}
}
// src/common/interceptors/cache.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
private cache = new Map<string, { data: any; expiry: number }>();
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const cacheKey = this.getCacheKey(request);
// Nur GET-Requests cachen
if (request.method !== 'GET') {
return next.handle();
}
// Cache-Hit prüfen
const cached = this.cache.get(cacheKey);
if (cached && cached.expiry > Date.now()) {
console.log(`Cache hit for ${cacheKey}`);
return of(cached.data);
}
// Cache-Miss - Request ausführen und Ergebnis cachen
return next.handle().pipe(
tap(data => {
const expiry = Date.now() + 5 * 60 * 1000; // 5 Minuten
this.cache.set(cacheKey, { data, expiry });
console.log(`Cached response for ${cacheKey}`);
// Cache-Cleanup (einfache Implementierung)
if (this.cache.size > 1000) {
this.cleanupCache();
}
}),
);
}
private getCacheKey(request: any): string {
return `${request.method}:${request.url}:${JSON.stringify(request.query)}`;
}
private cleanupCache(): void {
const now = Date.now();
for (const [key, value] of this.cache.entries()) {
if (value.expiry <= now) {
this.cache.delete(key);
}
}
}
}
// src/common/interceptors/error-handling.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';
@Injectable()
export class ErrorHandlingInterceptor implements NestInterceptor {
private readonly logger = new Logger(ErrorHandlingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
retry(2), // Retry bis zu 2 mal bei Fehlern
catchError(error => {
const request = context.switchToHttp().getRequest();
const { method, url, user } = request;
this.logger.error(
`Error in ${method} ${url}`,
{
error: error.message,
stack: error.stack,
user: user?.id,
timestamp: new Date().toISOString(),
},
);
// Error-Metrics sammeln (z.B. für Prometheus)
this.incrementErrorMetric(method, url, error.constructor.name);
return throwError(() => error);
}),
);
}
private incrementErrorMetric(method: string, url: string, errorType: string): void {
// Hier würde normalerweise ein Metrics-Service aufgerufen
console.log(`Error metric: ${method} ${url} ${errorType}`);
}
}Interceptors können auf verschiedenen Ebenen angewendet werden:
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ResponseTransformInterceptor } from './common/interceptors/response-transform.interceptor';
import { TimingInterceptor } from './common/interceptors/timing.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Globale Interceptors
app.useGlobalInterceptors(
new TimingInterceptor(),
new ResponseTransformInterceptor(),
);
await app.listen(3000);
}
bootstrap();
// Oder via Provider in AppModule
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: TimingInterceptor,
},
{
provide: APP_INTERCEPTOR,
useClass: ResponseTransformInterceptor,
},
],
})
export class AppModule {}// src/users/users.controller.ts
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { CacheInterceptor } from '../common/interceptors/cache.interceptor';
import { UserValidationInterceptor } from './interceptors/user-validation.interceptor';
@Controller('users')
@UseInterceptors(CacheInterceptor) // Für alle Methoden in diesem Controller
export class UsersController {
@Get()
@UseInterceptors(UserValidationInterceptor) // Nur für diese Methode
findAll() {
return this.usersService.findAll();
}
}// src/products/products.controller.ts
import { Controller, Get, Post, UseInterceptors } from '@nestjs/common';
@Controller('products')
export class ProductsController {
@Get()
@UseInterceptors(CacheInterceptor) // Nur für GET-Requests
findAll() {
return this.productsService.findAll();
}
@Post()
@UseInterceptors(AuditInterceptor, ValidationInterceptor) // Multiple Interceptors
create(@Body() createProductDto: CreateProductDto) {
return this.productsService.create(createProductDto);
}
}Guards sind spezielle Klassen, die das
CanActivate-Interface implementieren und bestimmen, ob ein
Request verarbeitet werden soll. Sie werden nach Middleware aber vor
Interceptors und Pipes ausgeführt und sind ideal für Authentication und
Authorization.
Authentication Guards verifizieren die Identität des Benutzers:
// src/auth/guards/jwt-auth.guard.ts
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UserService } from '../../users/services/user.service';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private userService: UserService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedException('No token provided');
}
try {
const token = authHeader.substring(7);
const payload = this.jwtService.verify(token);
// User aus Datenbank laden und zu Request hinzufügen
const user = await this.userService.findOne(payload.sub);
if (!user || !user.isActive) {
throw new UnauthorizedException('User not found or inactive');
}
request.user = user;
return true;
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
}
}
// src/auth/guards/local-auth.guard.ts
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { AuthService } from '../services/auth.service';
@Injectable()
export class LocalAuthGuard implements CanActivate {
constructor(private authService: AuthService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const { email, password } = request.body;
if (!email || !password) {
throw new UnauthorizedException('Email and password are required');
}
try {
const user = await this.authService.validateUser(email, password);
request.user = user;
return true;
} catch (error) {
throw new UnauthorizedException('Invalid credentials');
}
}
}
// src/auth/guards/api-key.guard.ts
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class ApiKeyGuard implements CanActivate {
constructor(private configService: ConfigService) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const apiKey = request.headers['x-api-key'] || request.query.apiKey;
if (!apiKey) {
throw new UnauthorizedException('API key is required');
}
const validApiKeys = this.configService.get<string[]>('API_KEYS') || [];
if (!validApiKeys.includes(apiKey)) {
throw new UnauthorizedException('Invalid API key');
}
return true;
}
}Authorization Guards prüfen, ob ein authentifizierter Benutzer die erforderlichen Berechtigungen hat:
// src/auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true; // Keine Rollen erforderlich
}
const { user } = context.switchToHttp().getRequest();
if (!user) {
return false; // Kein authentifizierter Benutzer
}
return requiredRoles.some(role => user.roles?.includes(role));
}
}
// src/auth/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
// src/auth/guards/permissions.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
export enum Permission {
READ_USERS = 'read:users',
WRITE_USERS = 'write:users',
DELETE_USERS = 'delete:users',
READ_PRODUCTS = 'read:products',
WRITE_PRODUCTS = 'write:products',
ADMIN_ACCESS = 'admin:access',
}
export const PERMISSIONS_KEY = 'permissions';
export const RequirePermissions = (...permissions: Permission[]) =>
SetMetadata(PERMISSIONS_KEY, permissions);
@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredPermissions = this.reflector.getAllAndOverride<Permission[]>(
PERMISSIONS_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredPermissions) {
return true;
}
const { user } = context.switchToHttp().getRequest();
if (!user) {
throw new ForbiddenException('Authentication required');
}
const hasPermission = requiredPermissions.every(permission =>
user.permissions?.includes(permission),
);
if (!hasPermission) {
throw new ForbiddenException(
`Required permissions: ${requiredPermissions.join(', ')}`,
);
}
return true;
}
}
// src/auth/guards/ownership.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
@Injectable()
export class OwnershipGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const { user, params } = request;
if (!user) {
return false;
}
// Super-Admin kann alles
if (user.roles.includes('super-admin')) {
return true;
}
// Resource-ID aus URL-Parameter
const resourceUserId = params.userId || params.id;
if (!resourceUserId) {
return true; // Keine spezifische Resource
}
// Prüfen, ob User die eigene Resource anfragt
if (user.id !== resourceUserId) {
throw new ForbiddenException('You can only access your own resources');
}
return true;
}
}Ein umfassendes RBAC-System mit hierarchischen Rollen:
// src/auth/rbac/rbac.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { RoleService } from './role.service';
export interface RbacRequirement {
roles?: string[];
permissions?: string[];
resource?: string;
action?: string;
}
export const RBAC_KEY = 'rbac';
export const RequireRbac = (requirement: RbacRequirement) =>
SetMetadata(RBAC_KEY, requirement);
@Injectable()
export class RbacGuard implements CanActivate {
constructor(
private reflector: Reflector,
private roleService: RoleService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const requirement = this.reflector.get<RbacRequirement>(
RBAC_KEY,
context.getHandler(),
);
if (!requirement) {
return true;
}
const { user } = context.switchToHttp().getRequest();
if (!user) {
return false;
}
// Rolle-basierte Prüfung
if (requirement.roles) {
const hasRole = await this.roleService.userHasAnyRole(
user.id,
requirement.roles,
);
if (!hasRole) {
return false;
}
}
// Permission-basierte Prüfung
if (requirement.permissions) {
const hasPermission = await this.roleService.userHasAnyPermission(
user.id,
requirement.permissions,
);
if (!hasPermission) {
return false;
}
}
// Resource-Action-basierte Prüfung
if (requirement.resource && requirement.action) {
const canAccess = await this.roleService.userCanAccessResource(
user.id,
requirement.resource,
requirement.action,
);
if (!canAccess) {
return false;
}
}
return true;
}
}
// src/auth/rbac/role.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Role } from './entities/role.entity';
import { Permission } from './entities/permission.entity';
import { UserRole } from './entities/user-role.entity';
@Injectable()
export class RoleService {
constructor(
@InjectRepository(Role) private roleRepository: Repository<Role>,
@InjectRepository(Permission) private permissionRepository: Repository<Permission>,
@InjectRepository(UserRole) private userRoleRepository: Repository<UserRole>,
) {}
async userHasAnyRole(userId: string, roleNames: string[]): Promise<boolean> {
const userRoles = await this.userRoleRepository
.createQueryBuilder('ur')
.innerJoin('ur.role', 'role')
.where('ur.userId = :userId', { userId })
.andWhere('role.name IN (:...roleNames)', { roleNames })
.getCount();
return userRoles > 0;
}
async userHasAnyPermission(userId: string, permissionNames: string[]): Promise<boolean> {
const permissions = await this.userRoleRepository
.createQueryBuilder('ur')
.innerJoin('ur.role', 'role')
.innerJoin('role.permissions', 'permission')
.where('ur.userId = :userId', { userId })
.andWhere('permission.name IN (:...permissionNames)', { permissionNames })
.getCount();
return permissions > 0;
}
async userCanAccessResource(
userId: string,
resource: string,
action: string,
): Promise<boolean> {
const permissionName = `${action}:${resource}`;
return this.userHasAnyPermission(userId, [permissionName]);
}
async getUserEffectivePermissions(userId: string): Promise<string[]> {
const permissions = await this.userRoleRepository
.createQueryBuilder('ur')
.innerJoin('ur.role', 'role')
.innerJoin('role.permissions', 'permission')
.select('permission.name')
.where('ur.userId = :userId', { userId })
.getRawMany();
return permissions.map(p => p.permission_name);
}
}
// Verwendung in Controllern
@Controller('users')
export class UsersController {
@Get()
@RequireRbac({ permissions: ['read:users'] })
@UseGuards(JwtAuthGuard, RbacGuard)
findAll() {
return this.usersService.findAll();
}
@Post()
@RequireRbac({ permissions: ['create:users'] })
@UseGuards(JwtAuthGuard, RbacGuard)
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
@Delete(':id')
@RequireRbac({
permissions: ['delete:users'],
resource: 'user',
action: 'delete',
})
@UseGuards(JwtAuthGuard, RbacGuard, OwnershipGuard)
remove(@Param('id') id: string) {
return this.usersService.remove(id);
}
}Pipes transformieren und validieren eingehende Daten, bevor sie den Route-Handler erreichen. Sie werden nach Guards aber vor dem Controller-Handler ausgeführt und sind essentiell für Datenintegrität und Sicherheit.
NestJS bietet mehrere eingebaute Pipes für häufige Anwendungsfälle:
// src/users/users.controller.ts
import {
Controller,
Get,
Post,
Body,
Param,
Query,
ParseIntPipe,
ParseUUIDPipe,
ParseBoolPipe,
ParseArrayPipe,
ParseEnumPipe,
DefaultValuePipe,
ValidationPipe,
} from '@nestjs/common';
export enum UserStatus {
ACTIVE = 'active',
INACTIVE = 'inactive',
SUSPENDED = 'suspended',
}
@Controller('users')
export class UsersController {
// ParseIntPipe für numerische Parameter
@Get('page/:page')
getPage(@Param('page', ParseIntPipe) page: number) {
return { page, type: typeof page }; // page ist number
}
// ParseUUIDPipe für UUID-Validierung
@Get(':id')
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.usersService.findOne(id);
}
// ParseBoolPipe für Boolean-Parameter
@Get()
findAll(
@Query('active', ParseBoolPipe) active: boolean,
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
) {
return this.usersService.findAll({ active, page });
}
// ParseArrayPipe für Array-Parameter
@Get('by-ids')
findByIds(
@Query('ids', new ParseArrayPipe({ items: String, separator: ',' }))
ids: string[],
) {
return this.usersService.findByIds(ids);
}
// ParseEnumPipe für Enum-Validierung
@Get('by-status/:status')
findByStatus(
@Param('status', new ParseEnumPipe(UserStatus))
status: UserStatus,
) {
return this.usersService.findByStatus(status);
}
// ValidationPipe für DTO-Validierung
@Post()
create(
@Body(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}))
createUserDto: CreateUserDto,
) {
return this.usersService.create(createUserDto);
}
}Custom Pipes für spezifische Validierungs- und Transformationsanforderungen:
// src/common/pipes/parse-date.pipe.ts
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
import { isValid, parseISO } from 'date-fns';
@Injectable()
export class ParseDatePipe implements PipeTransform<string, Date> {
transform(value: string): Date {
if (!value) {
throw new BadRequestException('Date value is required');
}
const date = parseISO(value);
if (!isValid(date)) {
throw new BadRequestException(`Invalid date format: ${value}. Expected ISO 8601 format.`);
}
return date;
}
}
// src/common/pipes/parse-json.pipe.ts
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseJsonPipe implements PipeTransform {
transform(value: string): any {
if (!value) {
return null;
}
try {
return JSON.parse(value);
} catch (error) {
throw new BadRequestException(`Invalid JSON format: ${error.message}`);
}
}
}
// src/common/pipes/file-validation.pipe.ts
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
interface FileValidationOptions {
maxSize?: number; // in bytes
allowedMimeTypes?: string[];
required?: boolean;
}
@Injectable()
export class FileValidationPipe implements PipeTransform {
constructor(private options: FileValidationOptions = {}) {}
transform(file: Express.Multer.File): Express.Multer.File {
if (!file) {
if (this.options.required) {
throw new BadRequestException('File is required');
}
return file;
}
// Größe prüfen
if (this.options.maxSize && file.size > this.options.maxSize) {
throw new BadRequestException(
`File size ${file.size} exceeds maximum allowed size ${this.options.maxSize}`,
);
}
// MIME-Type prüfen
if (this.options.allowedMimeTypes &&
!this.options.allowedMimeTypes.includes(file.mimetype)) {
throw new BadRequestException(
`File type ${file.mimetype} is not allowed. Allowed types: ${this.options.allowedMimeTypes.join(', ')}`,
);
}
return file;
}
}
// src/common/pipes/sanitization.pipe.ts
import { PipeTransform, Injectable } from '@nestjs/common';
import * as DOMPurify from 'isomorphic-dompurify';
@Injectable()
export class SanitizationPipe implements PipeTransform {
transform(value: any): any {
if (typeof value === 'string') {
return DOMPurify.sanitize(value);
}
if (typeof value === 'object' && value !== null) {
return this.sanitizeObject(value);
}
return value;
}
private sanitizeObject(obj: any): any {
if (Array.isArray(obj)) {
return obj.map(item => this.transform(item));
}
const sanitized = {};
for (const [key, value] of Object.entries(obj)) {
sanitized[key] = this.transform(value);
}
return sanitized;
}
}
// src/common/pipes/trim.pipe.ts
import { PipeTransform, Injectable } from '@nestjs/common';
@Injectable()
export class TrimPipe implements PipeTransform {
transform(value: any): any {
if (typeof value === 'string') {
return value.trim();
}
if (typeof value === 'object' && value !== null) {
return this.trimObject(value);
}
return value;
}
private trimObject(obj: any): any {
if (Array.isArray(obj)) {
return obj.map(item => this.transform(item));
}
const trimmed = {};
for (const [key, value] of Object.entries(obj)) {
trimmed[key] = this.transform(value);
}
return trimmed;
}
}Integration von class-validator für umfassende DTO-Validierung:
// src/users/dto/create-user.dto.ts
import {
IsEmail,
IsString,
IsOptional,
IsDateString,
IsPhoneNumber,
IsEnum,
IsBoolean,
MinLength,
MaxLength,
Matches,
IsArray,
ValidateNested,
IsNotEmpty,
} from 'class-validator';
import { Transform, Type } from 'class-transformer';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export enum UserRole {
USER = 'user',
ADMIN = 'admin',
MODERATOR = 'moderator',
}
export class AddressDto {
@ApiProperty()
@IsString()
@IsNotEmpty()
street: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
city: string;
@ApiProperty()
@IsString()
@Matches(/^\d{5}$/, { message: 'Postal code must be 5 digits' })
postalCode: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
country: string;
}
export class CreateUserDto {
@ApiProperty({ description: 'User email address' })
@IsEmail({}, { message: 'Please provide a valid email address' })
@Transform(({ value }) => value.toLowerCase().trim())
email: string;
@ApiProperty({ description: 'User password', minLength: 8 })
@IsString()
@MinLength(8, { message: 'Password must be at least 8 characters long' })
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/, {
message: 'Password must contain uppercase, lowercase, number and special character',
})
password: string;
@ApiProperty({ description: 'User first name' })
@IsString()
@MinLength(1, { message: 'First name cannot be empty' })
@MaxLength(50, { message: 'First name cannot exceed 50 characters' })
@Transform(({ value }) => value.trim())
firstName: string;
@ApiProperty({ description: 'User last name' })
@IsString()
@MinLength(1, { message: 'Last name cannot be empty' })
@MaxLength(50, { message: 'Last name cannot exceed 50 characters' })
@Transform(({ value }) => value.trim())
lastName: string;
@ApiPropertyOptional({ description: 'User phone number' })
@IsOptional()
@IsPhoneNumber('DE', { message: 'Please provide a valid German phone number' })
phoneNumber?: string;
@ApiPropertyOptional({ description: 'User birth date' })
@IsOptional()
@IsDateString({}, { message: 'Please provide a valid date' })
birthDate?: string;
@ApiPropertyOptional({ description: 'User role', enum: UserRole, default: UserRole.USER })
@IsOptional()
@IsEnum(UserRole, { message: 'Role must be one of: user, admin, moderator' })
role?: UserRole = UserRole.USER;
@ApiPropertyOptional({ description: 'User tags' })
@IsOptional()
@IsArray()
@IsString({ each: true })
@Transform(({ value }) => Array.isArray(value) ? value.map(tag => tag.trim()) : [])
tags?: string[];
@ApiPropertyOptional({ description: 'User address' })
@IsOptional()
@ValidateNested()
@Type(() => AddressDto)
address?: AddressDto;
@ApiPropertyOptional({ description: 'Newsletter subscription' })
@IsOptional()
@IsBoolean()
@Transform(({ value }) => {
if (typeof value === 'string') {
return value.toLowerCase() === 'true';
}
return Boolean(value);
})
newsletterSubscription?: boolean = false;
}
// src/common/pipes/advanced-validation.pipe.ts
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException,
Type,
} from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';
interface ValidationPipeOptions {
transform?: boolean;
whitelist?: boolean;
forbidNonWhitelisted?: boolean;
skipMissingProperties?: boolean;
groups?: string[];
dismissDefaultMessages?: boolean;
validationError?: {
target?: boolean;
value?: boolean;
};
}
@Injectable()
export class AdvancedValidationPipe implements PipeTransform<any> {
constructor(private options: ValidationPipeOptions = {}) {}
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value;
}
// Transform plain object to class instance
const object = plainToInstance(metatype, value, {
enableImplicitConversion: this.options.transform,
});
// Validate the object
const errors = await validate(object, {
whitelist: this.options.whitelist,
forbidNonWhitelisted: this.options.forbidNonWhitelisted,
skipMissingProperties: this.options.skipMissingProperties,
groups: this.options.groups,
dismissDefaultMessages: this.options.dismissDefaultMessages,
validationError: this.options.validationError,
});
if (errors.length > 0) {
const errorMessages = this.formatErrors(errors);
throw new BadRequestException({
message: 'Validation failed',
errors: errorMessages,
});
}
return this.options.transform ? object : value;
}
private toValidate(metatype: Type): boolean {
const types: Type[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
private formatErrors(errors: any[]): any {
return errors.reduce((acc, error) => {
const property = error.property;
const constraints = error.constraints || {};
const children = error.children || [];
if (Object.keys(constraints).length > 0) {
acc[property] = Object.values(constraints);
}
if (children.length > 0) {
acc[property] = this.formatErrors(children);
}
return acc;
}, {});
}
}
// Verwendung mit verschiedenen Validation-Gruppen
export class UpdateUserDto {
@IsEmail({}, { groups: ['email-update'] })
@IsOptional({ groups: ['partial-update'] })
email?: string;
@MinLength(8, { groups: ['password-update'] })
@IsOptional({ groups: ['partial-update'] })
password?: string;
@IsString({ groups: ['profile-update', 'partial-update'] })
@IsOptional()
firstName?: string;
}
@Controller('users')
export class UsersController {
@Patch(':id/email')
updateEmail(
@Param('id', ParseUUIDPipe) id: string,
@Body(new AdvancedValidationPipe({
groups: ['email-update'],
whitelist: true,
}))
updateDto: UpdateUserDto,
) {
return this.usersService.updateEmail(id, updateDto);
}
@Patch(':id')
update(
@Param('id', ParseUUIDPipe) id: string,
@Body(new AdvancedValidationPipe({
groups: ['partial-update'],
whitelist: true,
transform: true,
}))
updateDto: UpdateUserDto,
) {
return this.usersService.update(id, updateDto);
}
}Exception Filters fangen unbehandelte Exceptions ab und transformieren sie in standardisierte HTTP-Responses. Sie werden am Ende der Request/Response-Pipeline ausgeführt und sorgen für einheitliche Fehlerbehandlung.
NestJS bietet einen eingebauten Global Exception Filter, der HTTP-Exceptions automatisch behandelt:
// Built-in HTTP Exceptions
import {
BadRequestException,
UnauthorizedException,
ForbiddenException,
NotFoundException,
ConflictException,
InternalServerErrorException,
HttpStatus,
} from '@nestjs/common';
@Controller('examples')
export class ExamplesController {
@Get('bad-request')
throwBadRequest() {
throw new BadRequestException('This is a bad request');
}
@Get('unauthorized')
throwUnauthorized() {
throw new UnauthorizedException('You are not authorized');
}
@Get('forbidden')
throwForbidden() {
throw new ForbiddenException('Access denied');
}
@Get('not-found')
throwNotFound() {
throw new NotFoundException('Resource not found');
}
@Get('conflict')
throwConflict() {
throw new ConflictException('Resource already exists');
}
@Get('custom-exception')
throwCustomException() {
throw new HttpException({
status: HttpStatus.I_AM_A_TEAPOT,
error: 'I am a teapot',
message: 'Cannot brew coffee with a teapot',
}, HttpStatus.I_AM_A_TEAPOT);
}
}Custom Exception Filters für spezifische Fehlerbehandlung:
// src/common/filters/http-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(HttpExceptionFilter.name);
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
// Error-Details extrahieren
const errorMessage = typeof exceptionResponse === 'string'
? exceptionResponse
: (exceptionResponse as any).message || exception.message;
const errorDetails = typeof exceptionResponse === 'object'
? exceptionResponse
: { message: errorMessage };
// Request-Details für Logging
const { method, url, ip, headers } = request;
const userAgent = headers['user-agent'] || 'Unknown';
const requestId = headers['x-request-id'] || 'No ID';
// Error-Response zusammenstellen
const errorResponse = {
success: false,
statusCode: status,
timestamp: new Date().toISOString(),
path: url,
method,
requestId,
error: {
name: exception.constructor.name,
message: errorMessage,
...errorDetails,
},
...(process.env.NODE_ENV === 'development' && {
stack: exception.stack,
}),
};
// Logging
this.logger.error(
`HTTP ${status} Error - ${method} ${url}`,
{
...errorResponse,
ip,
userAgent,
},
);
response.status(status).json(errorResponse);
}
}
// src/common/filters/validation-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
BadRequestException,
} from '@nestjs/common';
import { Response } from 'express';
@Catch(BadRequestException)
export class ValidationExceptionFilter implements ExceptionFilter {
catch(exception: BadRequestException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest();
const status = exception.getStatus();
const exceptionResponse = exception.getResponse() as any;
// Validation-spezifische Response
const errorResponse = {
success: false,
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
error: {
type: 'ValidationError',
message: 'Request validation failed',
details: exceptionResponse.message || exceptionResponse,
},
};
response.status(status).json(errorResponse);
}
}
// src/common/filters/database-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpStatus,
Logger,
} from '@nestjs/common';
import { QueryFailedError, EntityNotFoundError } from 'typeorm';
@Catch(QueryFailedError, EntityNotFoundError)
export class DatabaseExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(DatabaseExceptionFilter.name);
catch(exception: QueryFailedError | EntityNotFoundError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message = 'Database error occurred';
let code = 'DATABASE_ERROR';
if (exception instanceof EntityNotFoundError) {
status = HttpStatus.NOT_FOUND;
message = 'Resource not found';
code = 'ENTITY_NOT_FOUND';
} else if (exception instanceof QueryFailedError) {
// PostgreSQL-spezifische Error-Codes
const pgError = exception as any;
switch (pgError.code) {
case '23505': // unique_violation
status = HttpStatus.CONFLICT;
message = 'Resource already exists';
code = 'DUPLICATE_ENTRY';
break;
case '23503': // foreign_key_violation
status = HttpStatus.BAD_REQUEST;
message = 'Invalid reference';
code = 'FOREIGN_KEY_VIOLATION';
break;
case '23502': // not_null_violation
status = HttpStatus.BAD_REQUEST;
message = 'Required field missing';
code = 'NOT_NULL_VIOLATION';
break;
default:
this.logger.error('Unhandled database error', {
code: pgError.code,
message: pgError.message,
detail: pgError.detail,
});
}
}
const errorResponse = {
success: false,
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
error: {
code,
message,
...(process.env.NODE_ENV === 'development' && {
details: exception.message,
}),
},
};
response.status(status).json(errorResponse);
}
}
// src/common/filters/all-exceptions.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
private readonly logger = new Logger(AllExceptionsFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message = 'Internal server error';
let error = 'INTERNAL_ERROR';
if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
message = typeof exceptionResponse === 'string'
? exceptionResponse
: (exceptionResponse as any).message;
error = exception.constructor.name;
} else if (exception instanceof Error) {
message = exception.message;
error = exception.constructor.name;
// Log unexpected errors
this.logger.error('Unexpected error occurred', {
error: exception.message,
stack: exception.stack,
url: request.url,
method: request.method,
});
}
const errorResponse = {
success: false,
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
requestId: request.headers['x-request-id'],
error: {
type: error,
message,
},
};
response.status(status).json(errorResponse);
}
}Eine strukturierte Exception-Hierarchie für bessere Fehlerbehandlung:
// src/common/exceptions/base.exception.ts
export abstract class BaseException extends Error {
abstract readonly statusCode: number;
abstract readonly errorCode: string;
constructor(
message: string,
public readonly context?: Record<string, any>,
) {
super(message);
this.name = this.constructor.name;
}
}
// src/common/exceptions/business.exceptions.ts
export class BusinessException extends BaseException {
readonly statusCode = 400;
readonly errorCode = 'BUSINESS_RULE_VIOLATION';
}
export class InsufficientFundsException extends BusinessException {
readonly errorCode = 'INSUFFICIENT_FUNDS';
constructor(accountId: string, requestedAmount: number, availableAmount: number) {
super(
`Insufficient funds in account ${accountId}`,
{ accountId, requestedAmount, availableAmount },
);
}
}
export class InvalidStateTransitionException extends BusinessException {
readonly errorCode = 'INVALID_STATE_TRANSITION';
constructor(entity: string, currentState: string, targetState: string) {
super(
`Cannot transition ${entity} from ${currentState} to ${targetState}`,
{ entity, currentState, targetState },
);
}
}
// src/common/exceptions/domain.exceptions.ts
export class DomainException extends BaseException {
readonly statusCode = 422;
readonly errorCode = 'DOMAIN_RULE_VIOLATION';
}
export class AgeRestrictionException extends DomainException {
readonly errorCode = 'AGE_RESTRICTION_VIOLATION';
constructor(minAge: number, actualAge: number) {
super(
`Minimum age requirement of ${minAge} years not met`,
{ minAge, actualAge },
);
}
}
export class EmailAlreadyExistsException extends DomainException {
readonly statusCode = 409;
readonly errorCode = 'EMAIL_ALREADY_EXISTS';
constructor(email: string) {
super(`Email address ${email} is already registered`, { email });
}
}
// src/common/filters/business-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';
import { BaseException } from '../exceptions/base.exception';
@Catch(BaseException)
export class BusinessExceptionFilter implements ExceptionFilter {
catch(exception: BaseException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const errorResponse = {
success: false,
statusCode: exception.statusCode,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
error: {
code: exception.errorCode,
message: exception.message,
...(exception.context && { context: exception.context }),
},
};
response.status(exception.statusCode).json(errorResponse);
}
}
// Globale Filter-Registrierung
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Exception Filters in Reihenfolge der Spezifität
app.useGlobalFilters(
new BusinessExceptionFilter(),
new DatabaseExceptionFilter(),
new ValidationExceptionFilter(),
new HttpExceptionFilter(),
new AllExceptionsFilter(), // Muss zuletzt sein
);
await app.listen(3000);
}
// Oder via Provider
@Module({
providers: [
{
provide: APP_FILTER,
useClass: BusinessExceptionFilter,
},
{
provide: APP_FILTER,
useClass: AllExceptionsFilter,
},
],
})
export class AppModule {}Die Request/Response-Pipeline von NestJS mit Middleware, Interceptors, Guards, Pipes und Exception Filters bietet eine mächtige und flexible Architektur für die Behandlung von Cross-Cutting Concerns. Durch das Verständnis der Ausführungsreihenfolge und der spezifischen Einsatzgebiete jeder Komponente können robuste, sichere und wartbare Anwendungen entwickelt werden, die moderne Best Practices befolgen und gleichzeitig hochgradig anpassbar bleiben.