Einführung
NestJS ist heute das meistgenutzte Node.js-Backend-Framework für den Bau skalierbarer, stark typisierter, wartbarer serverseitiger Anwendungen. Es kombiniert die solidesten Konzepte der Software-Engineering — Dependency Injection, Module, Schichtenarchitektur — mit der Produktivität von TypeScript und einem Ökosystem, das praktisch jeden Bedarf abdeckt: REST, GraphQL, WebSocket, Microservices, CLI, Cron-Jobs.
In diesem Guide bauen wir Schritt für Schritt eine vollständige, produktionsreife REST API: eine Blog-API mit JWT-Authentifizierung, Refresh Token, rollenbasierter Zugriffskontrolle (RBAC), Validierung, Datei-Uploads, zentralisiertem Logging und strukturierter Fehlerbehandlung — genau der Stack hinter den meisten echten NestJS-APIs im Jahr 2026.
Was Ist NestJS
NestJS ist ein opinionated Framework, das auf Express (oder alternativ Fastify) aufbaut und der Anwendung eine präzise Struktur auferlegt: Jede Funktion ist in Modulen organisiert, jedes Modul stellt Controller (die HTTP-Requests behandeln) und Provider (die die Geschäftslogik enthalten, typischerweise Services) bereit, die über ein integriertes, von Angular inspiriertes Dependency-Injection-System miteinander verbunden sind.
Warum Es Verwenden
- Natives TypeScript: durchgängige Typisierung, Autovervollständigung, sicheres Refactoring.
- Erzwungene Architektur: anders als reines Express zwingt NestJS zur Trennung der Zuständigkeiten (Controller/Service/Repository) und reduziert den typischen "big ball of mud" von Node-Projekten, die ohne Struktur gewachsen sind.
- Integrierte Dependency Injection: hohe Testbarkeit, geringe Kopplung, austauschbare Provider (nützlich für Mocks in Tests).
- Ausgereiftes Ökosystem: offizielle Module für TypeORM, Prisma, Mongoose, Passport, GraphQL, WebSocket, Bull/BullMQ, Swagger, gRPC, Microservices.
- Deklarative Decorators: Guards, Interceptors, Pipes und Exception Filters ermöglichen Cross-Cutting Concerns (Auth, Logging, Validierung), ohne die Geschäftslogik zu verunreinigen.
Wann Es Sich Lohnt (und Wann Nicht)
NestJS lohnt sich, wenn das Projekt eine Komplexität hat, die eine starre Struktur rechtfertigt: Teams mit mehreren Entwicklern, APIs, die im Laufe der Zeit wachsen sollen, hoher Bedarf an Testbarkeit, Enterprise-Anwendungen mit Sicherheits- und Compliance-Anforderungen. Für ein einmaliges Skript oder einen Prototyp, der in einer Woche weggeworfen wird, ist es wahrscheinlich übertrieben — dort bleiben reines Express oder "nacktes" Fastify schneller aufzusetzen.
Vor- und Nachteile
| Vorteile | Nachteile |
|---|---|
| Klare, konsistente Struktur über verschiedene Projekte hinweg | Steilere Lernkurve als reines Express (Decorators, DI, Module) |
| Native Testbarkeit dank DI | Boilerplate-Overhead bei sehr kleinen Projekten |
| Breites, gut gepflegtes offizielles Ökosystem | Mehr "Magie" (Decorators, Reflection) im Vergleich zu explizitem Code |
| Hervorragende Integration mit TypeScript und Swagger/OpenAPI | Etwas höhere Bundle-Größe und Cold-Start als minimalistische Frameworks (relevant in serverlosen Umgebungen) |
| Erleichtert die Einführung von Clean/Hexagonal Architecture | Erfordert Teamdisziplin, um die Flexibilität der Module nicht zu missbrauchen |
Reale Anwendungsfälle
NestJS wird in der Produktion von Unternehmen wie Adidas, Roche, Autodesk und Decathlon eingesetzt und ist zudem eine sehr verbreitete Wahl für B2B-SaaS-Backends, E-Commerce-Plattformen, Benutzerverwaltungssysteme und Microservice-APIs, die über gRPC oder Message Queues (RabbitMQ, Kafka) miteinander kommunizieren müssen.
Voraussetzungen
Bevor du beginnst, stelle sicher, dass diese Tools installiert sind und du die Grundlagen von TypeScript (Interfaces, Decorators, Generics) sowie REST-Konzepte (HTTP-Verben, Statuscodes, Idempotenz) kennst.
| Tool | Empfohlene Version | Zweck |
|---|---|---|
| Node.js | 20.x LTS oder höher | JavaScript-Runtime, auf der NestJS läuft |
| npm | 10.x (in Node 20 enthalten) | Paketverwaltung |
| NestJS CLI | @nestjs/cli 10.x+ | Scaffolding von Modulen, Controllern, Services |
| TypeScript | 5.x | Sprache, in der NestJS und deine App geschrieben sind |
| VS Code | Neueste stabile Version | Editor mit nativer TypeScript-Unterstützung |
| PostgreSQL | 15.x oder höher | Relationale Datenbank für das TypeORM-Beispiel |
| Docker (optional) | 24.x+ | PostgreSQL lokal ohne native Installation ausführen |
# Installierte Versionen prüfen
node -v
npm -v
# NestJS CLI global installieren
npm install -g @nestjs/cli
nest --version
Tipp: wenn du PostgreSQL nicht nativ installieren möchtest, starte es mit Docker:
docker run --name pg-blog -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=blog_db -p 5432:5432 -d postgres:16.
Architektur
Bevor du Code schreibst, ist es essenziell zu verstehen, wie NestJS einen HTTP-Request durch seine Hauptbausteine leitet. Hier der vollständige Lebenszyklus eines Requests:
Client
│
▼
┌─────────────┐
│ Middleware │ (z. B. Logger, cookie-parser — läuft vor dem Routing)
└──────┬───────┘
▼
┌─────────────┐
│ Guard │ (z. B. AuthGuard — entscheidet, ob der Request fortfahren darf)
└──────┬───────┘
▼
┌─────────────┐
│ Interceptor │ ("Vorher"-Phase — z. B. Logging, Input-Transformation)
└──────┬───────┘
▼
┌─────────────┐
│ Pipe │ (validiert und transformiert eingehende Parameter)
└──────┬───────┘
▼
┌─────────────┐
│ Controller │ (empfängt den Request, delegiert an den Service)
└──────┬───────┘
▼
┌─────────────┐
│ Service │ (Geschäftslogik, ruft die Repositories auf)
└──────┬───────┘
▼
┌─────────────┐
│ Repository / │ (Datenzugriff — TypeORM, Prisma, Mongoose...)
│ Database │
└──────┬───────┘
▼
┌─────────────┐
│ Interceptor │ ("Nachher"-Phase — z. B. Response-Transformation)
└──────┬───────┘
▼
┌──────────────────┐
│ Exception Filter │ (fängt NUR ab, wenn eine Exception geworfen wird)
└──────┬────────────┘
▼
Response
Controller
Die äußerste Schicht: empfängt HTTP-Requests, extrahiert Parameter/Body/Query und delegiert die Logik sofort an den entsprechenden Service. Ein Controller sollte niemals Geschäftslogik enthalten — seine einzige Verantwortung ist das Mapping HTTP ↔ Methodenaufruf.
Service
Enthält die eigentliche Geschäftslogik. Es ist ein injizierbarer Provider,
typischerweise mit @Injectable() markiert, und wird über den Konstruktor in den
Controller (oder andere Services) injiziert.
Module
Das Modul ist die organisatorische Einheit von NestJS: Es gruppiert Controller, Provider und
Imports anderer Module. Jede Anwendung hat mindestens ein Root-AppModule, und
Funktionalitäten werden typischerweise in Feature-Module isoliert (z. B.
PostsModule, AuthModule, UsersModule).
Provider
Jede Klasse, die vom Dependency-Injection-Container von Nest verwaltet wird: Services,
Repositories, Factories, Helper. Sie wird im Modul unter providers deklariert
und kann überall dort injiziert werden, wo sie verfügbar ist (im selben Modul oder exportiert
in andere Module).
Middleware
Funktionen, die vor dem Routing von Nest ausgeführt werden, mit direktem
Zugriff auf req, res und next() — dasselbe Modell wie
Express. Nützlich für rohes Logging, Cookie-Parsing oder global angewandte
Custom-Header.
Guard
Entscheiden, ob ein Request fortfahren darf, indem sie true/
false zurückgeben (oder eine Exception werfen). Sie sind der richtige Ort für
Authentifizierung und Autorisierung — niemals im Controller oder Service.
Interceptor
Positionieren sich um die Ausführung des Route-Handlers herum (wie AOP-Middleware): Sie können den Request transformieren, bevor er den Controller erreicht, und die Response, bevor sie hinausgeht. Typische Anwendungsfälle: Logging von Antwortzeiten, einheitliche Response-Transformation, Caching, Timeout-Handling.
Pipe
Transformieren und validieren eingehende Daten (Routenparameter, Query-Strings, Body), bevor
sie den Controller erreichen. ValidationPipe, integriert mit
class-validator, ist bei Weitem die meistgenutzte Pipe in jeder ernsthaften
NestJS-API.
Exception Filter
Fangen Exceptions ab, die an beliebiger Stelle im Request-Lebenszyklus geworfen werden, und wandeln sie in eine konsistente HTTP-Response um (Statuscode, strukturierter JSON-Body), wodurch verhindert wird, dass rohe Stack Traces oder Fehler den Client erreichen.
Installation
# 1. Erstelle ein neues NestJS-Projekt
nest new blog-api
# Wähle während der Erstellung npm als Package Manager, wenn gefragt
cd blog-api
# 2. Installiere die Abhängigkeiten für die Datenbank (TypeORM + PostgreSQL-Treiber)
npm install @nestjs/typeorm typeorm pg
# 3. Installiere die Abhängigkeiten für die Validierung
npm install class-validator class-transformer
# 4. Installiere die Abhängigkeiten für die JWT-Authentifizierung
npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt
npm install --save-dev @types/passport-jwt @types/bcrypt
# 5. Installiere die Abhängigkeiten für Sicherheit und Rate Limiting
npm install helmet @nestjs/throttler
# 6. Installiere die Abhängigkeiten für Datei-Uploads
npm install @nestjs/platform-express multer
npm install --save-dev @types/multer
# 7. Installiere Swagger für die automatische API-Dokumentation
npm install @nestjs/swagger
# 8. Installiere den strukturierten Logger
npm install nestjs-pino pino-http pino-pretty
# 9. Richte Umgebungsvariablen ein
npm install @nestjs/config
Jeder Befehl installiert einen präzisen Funktionsblock: @nestjs/typeorm +
typeorm + pg verbinden Nest über den TypeORM-ORM mit PostgreSQL;
class-validator/class-transformer aktivieren automatisch validierte
DTOs; der Stack passport/passport-jwt/bcrypt baut den
gesamten Authentifizierungsablauf auf; helmet und
@nestjs/throttler härten die HTTP-Sicherheit und das Rate Limiting;
multer übernimmt multipart/form-data für Uploads; nestjs-pino
liefert strukturiertes JSON-Logging, geeignet für die Produktion.
Schritt-für-Schritt-Implementierung
1. Ordnerstruktur
src/
├── main.ts
├── app.module.ts
├── config/
│ └── configuration.ts
├── common/
│ ├── filters/
│ │ └── http-exception.filter.ts
│ ├── interceptors/
│ │ ├── logging.interceptor.ts
│ │ └── transform.interceptor.ts
│ ├── guards/
│ │ └── roles.guard.ts
│ ├── decorators/
│ │ └── roles.decorator.ts
│ └── pipes/
│ └── parse-object-id.pipe.ts
├── auth/
│ ├── auth.module.ts
│ ├── auth.controller.ts
│ ├── auth.service.ts
│ ├── strategies/
│ │ ├── jwt.strategy.ts
│ │ └── jwt-refresh.strategy.ts
│ └── dto/
│ ├── login.dto.ts
│ └── register.dto.ts
├── users/
│ ├── users.module.ts
│ ├── users.service.ts
│ └── entities/
│ └── user.entity.ts
└── posts/
├── posts.module.ts
├── posts.controller.ts
├── posts.service.ts
├── entities/
│ └── post.entity.ts
└── dto/
├── create-post.dto.ts
└── update-post.dto.ts
2. Zentralisierte Konfiguration mit @nestjs/config
// src/config/configuration.ts
export default () => ({
port: parseInt(process.env.PORT ?? '3000', 10),
database: {
host: process.env.DB_HOST ?? 'localhost',
port: parseInt(process.env.DB_PORT ?? '5432', 10),
username: process.env.DB_USERNAME ?? 'postgres',
password: process.env.DB_PASSWORD ?? 'postgres',
name: process.env.DB_NAME ?? 'blog_db',
},
jwt: {
accessSecret: process.env.JWT_ACCESS_SECRET ?? 'change-me-access',
accessExpiresIn: process.env.JWT_ACCESS_EXPIRES_IN ?? '15m',
refreshSecret: process.env.JWT_REFRESH_SECRET ?? 'change-me-refresh',
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN ?? '7d',
},
});
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ThrottlerModule } from '@nestjs/throttler';
import configuration from './config/configuration';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';
import { PostsModule } from './posts/posts.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, load: [configuration] }),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (cfg: ConfigService) => ({
type: 'postgres',
host: cfg.get('database.host'),
port: cfg.get('database.port'),
username: cfg.get('database.username'),
password: cfg.get('database.password'),
database: cfg.get('database.name'),
autoLoadEntities: true,
synchronize: process.env.NODE_ENV !== 'production', // ⚠️ nur in der Entwicklung
}),
}),
ThrottlerModule.forRoot([{ ttl: 60000, limit: 100 }]),
AuthModule,
UsersModule,
PostsModule,
],
})
export class AppModule {}
Zeile für Zeile: ConfigModule.forRoot({ isGlobal: true }) macht
den ConfigService in der gesamten Anwendung verfügbar, ohne das Modul überall
erneut importieren zu müssen. TypeOrmModule.forRootAsync baut die
Datenbankverbindung asynchron auf und liest die Werte aus dem ConfigService
statt aus einem statischen Objekt — notwendig, um Umgebungsvariablen verwenden zu können.
synchronize: true lässt TypeORM Tabellen automatisch basierend auf den Entities
erstellen/aktualisieren: sehr praktisch in der Entwicklung, gefährlich in der
Produktion (kann Daten löschen), wo stattdessen Migrationen verwendet werden.
3. Entities mit TypeORM
// src/users/entities/user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, OneToMany } from 'typeorm';
import { Post } from '../../posts/entities/post.entity';
import { Exclude } from 'class-transformer';
export enum UserRole {
ADMIN = 'admin',
AUTHOR = 'author',
READER = 'reader',
}
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
email: string;
@Column()
@Exclude() // schließt den Passwort-Hash aus serialisierten Responses aus
passwordHash: string;
@Column({ type: 'enum', enum: UserRole, default: UserRole.READER })
role: UserRole;
@OneToMany(() => Post, (post) => post.author)
posts: Post[];
@CreateDateColumn()
createdAt: Date;
}
// src/posts/entities/post.entity.ts
import {
Entity, Column, PrimaryGeneratedColumn, ManyToOne,
CreateDateColumn, UpdateDateColumn, Index,
} from 'typeorm';
import { User } from '../../users/entities/user.entity';
@Entity('posts')
export class Post {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
title: string;
@Index({ unique: true })
@Column()
slug: string;
@Column('text')
content: string;
@Column({ default: false })
published: boolean;
@Column({ nullable: true })
coverImageUrl?: string;
@ManyToOne(() => User, (user) => user.posts, { eager: true })
author: User;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
4. DTOs mit class-validator
// src/posts/dto/create-post.dto.ts
import { IsString, IsBoolean, IsOptional, MinLength, MaxLength } from 'class-validator';
export class CreatePostDto {
@IsString()
@MinLength(5)
@MaxLength(200)
title: string;
@IsString()
@MinLength(20)
content: string;
@IsBoolean()
@IsOptional()
published?: boolean;
}
// src/posts/dto/update-post.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreatePostDto } from './create-post.dto';
export class UpdatePostDto extends PartialType(CreatePostDto) {}
PartialType generiert automatisch eine Version, in der alle Felder des
ursprünglichen DTOs optional werden — perfekt für partielle Updates
(PATCH), ohne Validierungs-Decorators zu duplizieren.
5. ValidationPipe Global Aktivieren
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import helmet from 'helmet';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(helmet());
app.enableCors({ origin: process.env.CORS_ORIGIN?.split(',') ?? '*' });
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // entfernt Properties, die nicht im DTO enthalten sind
forbidNonWhitelisted: true, // wirft einen Fehler, wenn zusätzliche Properties ankommen
transform: true, // konvertiert Typen automatisch (z. B. string → number)
}),
);
app.useGlobalFilters(new HttpExceptionFilter());
app.setGlobalPrefix('api/v1');
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
whitelist: true + forbidNonWhitelisted: true sind das wichtigste
Paar für die DTO-Sicherheit: Ohne dies könnte ein Client zusätzliche Felder senden (z. B.
role: 'admin' in einer Registrierungsanfrage), die TypeORM versehentlich
persistieren könnte, wenn der Code sie nicht anderswo explizit filtert.
6. Custom Middleware
// src/common/middleware/request-id.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { randomUUID } from 'crypto';
@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction): void {
req['requestId'] = randomUUID();
res.setHeader('X-Request-Id', req['requestId']);
next();
}
}
// Registrierung in app.module.ts
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestIdMiddleware).forRoutes('*');
}
}
7. Guards: JwtAuthGuard und RolesGuard
// src/auth/strategies/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: config.get('jwt.accessSecret'),
});
}
async validate(payload: { sub: string; email: string; role: string }) {
// Der zurückgegebene Wert wird an req.user angehängt
return { userId: payload.sub, email: payload.email, role: payload.role };
}
}
// src/common/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { UserRole } from '../../users/entities/user.entity';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles);
// src/common/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';
import { UserRole } from '../../users/entities/user.entity';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) return true; // keine Rolleneinschränkung auf dieser Route
const { user } = context.switchToHttp().getRequest();
return requiredRoles.includes(user?.role);
}
}
Das Reflector.getAllAndOverride-Pattern liest Metadaten, die vom
@Roles(...)-Decorator sowohl auf Methoden- als auch auf Klassenebene gesetzt
wurden, und erlaubt es, eine Rolleneinschränkung auf einem ganzen Controller zu definieren
und bei Bedarf auf einzelnen Routen zu überschreiben.
8. Interceptors: Logging und Response-Transformation
// src/common/interceptors/logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger('HTTP');
intercept(context: ExecutionContext, next: CallHandler): Observable {
const req = context.switchToHttp().getRequest();
const start = Date.now();
return next.handle().pipe(
tap(() => {
const ms = Date.now() - start;
this.logger.log(`${req.method} ${req.url} — ${ms}ms`);
}),
);
}
}
// src/common/interceptors/transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response {
success: true;
data: T;
timestamp: string;
}
@Injectable()
export class TransformInterceptor implements NestInterceptor> {
intercept(context: ExecutionContext, next: CallHandler): Observable> {
return next.handle().pipe(
map((data) => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
);
}
}
9. Globaler Exception Filter
// src/common/filters/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger('ExceptionFilter');
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message = exception instanceof HttpException
? exception.getResponse()
: 'Errore interno del server';
// Loggt den vollständigen Stack Trace nur serverseitig, niemals in der Response an den Client
if (!(exception instanceof HttpException)) {
this.logger.error(exception instanceof Error ? exception.stack : exception);
}
response.status(status).json({
success: false,
statusCode: status,
path: request.url,
timestamp: new Date().toISOString(),
message,
});
}
}
Ein entscheidendes Sicherheitsdetail: Wenn die Exception keine bekannte
HttpException ist (also ein unerwarteter Fehler, z. B. ein Bug oder ein
Datenbankfehler), ist die an den Client zurückgegebene Nachricht generisch ("Interner
Serverfehler") — der echte Stack Trace wird nur serverseitig geloggt. Das Offenlegen von
Stack Traces gegenüber Clients ist ein bekanntes Sicherheitsproblem (Information Disclosure).
10. Custom Pipe
// src/common/pipes/parse-uuid-or-404.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, NotFoundException } from '@nestjs/common';
import { isUUID } from 'class-validator';
@Injectable()
export class ParseUuidOr404Pipe implements PipeTransform {
transform(value: string, _metadata: ArgumentMetadata): string {
if (!isUUID(value)) {
throw new NotFoundException(`Risorsa con id "${value}" non trovata`);
}
return value;
}
}
Einen 404 statt eines generischen 400 Bad Request zurückzugeben,
wenn die ID nicht einmal eine gültige UUID ist, ist eine bewusste Entscheidung: Aus Sicht des
Clients ist "Ressource nicht gefunden" semantisch korrekter und gibt keine Details über das
interne ID-Format preis.
Reales Beispiel: Eine Vollständige Blog-API
Fügen wir alle Teile in ein vollständiges PostsModule zusammen, mit CRUD,
Authentifizierung, rollenbasierter Autorisierung und Upload des Titelbilds.
Auth Service: Login, Register, Refresh Token
// src/auth/auth.service.ts
import { Injectable, UnauthorizedException, ConflictException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import * as bcrypt from 'bcrypt';
import { UsersService } from '../users/users.service';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
private config: ConfigService,
) {}
async register(dto: RegisterDto) {
const existing = await this.usersService.findByEmail(dto.email);
if (existing) throw new ConflictException('Email già registrata');
const passwordHash = await bcrypt.hash(dto.password, 12);
const user = await this.usersService.create({ ...dto, passwordHash });
return this.buildTokens(user.id, user.email, user.role);
}
async login(dto: LoginDto) {
const user = await this.usersService.findByEmail(dto.email);
if (!user) throw new UnauthorizedException('Credenziali non valide');
const passwordValid = await bcrypt.compare(dto.password, user.passwordHash);
if (!passwordValid) throw new UnauthorizedException('Credenziali non valide');
return this.buildTokens(user.id, user.email, user.role);
}
async refresh(userId: string, email: string, role: string) {
// In Produktion: prüfe auch, ob der Refresh Token serverseitig noch gültig ist
// (Whitelist/Blacklist), um ihn widerrufen zu können, z. B. beim Logout.
return this.buildTokens(userId, email, role);
}
private buildTokens(sub: string, email: string, role: string) {
const payload = { sub, email, role };
const accessToken = this.jwtService.sign(payload, {
secret: this.config.get('jwt.accessSecret'),
expiresIn: this.config.get('jwt.accessExpiresIn'),
});
const refreshToken = this.jwtService.sign(payload, {
secret: this.config.get('jwt.refreshSecret'),
expiresIn: this.config.get('jwt.refreshExpiresIn'),
});
return { accessToken, refreshToken };
}
}
Warum zwei separate Tokens? Der Access Token hat eine kurze Lebensdauer (15 Minuten) und wird bei jedem Request mitgesendet — wird er gestohlen, ist der Schaden zeitlich begrenzt. Der Refresh Token hat eine längere Lebensdauer (Tage), wird aber nur verwendet, um über einen dedizierten Endpunkt einen neuen Access Token zu erhalten, wodurch die Angriffsfläche reduziert wird.
Auth Controller
// src/auth/auth.controller.ts
import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('register')
register(@Body() dto: RegisterDto) {
return this.authService.register(dto);
}
@Post('login')
login(@Body() dto: LoginDto) {
return this.authService.login(dto);
}
@UseGuards(AuthGuard('jwt-refresh'))
@Post('refresh')
refresh(@Req() req) {
return this.authService.refresh(req.user.userId, req.user.email, req.user.role);
}
}
Posts Controller: CRUD, RBAC und Upload
// src/posts/posts.controller.ts
import {
Controller, Get, Post, Body, Patch, Param, Delete,
UseGuards, UseInterceptors, UploadedFile, Query,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { AuthGuard } from '@nestjs/passport';
import { PostsService } from './posts.service';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { Roles } from '../common/decorators/roles.decorator';
import { RolesGuard } from '../common/guards/roles.guard';
import { UserRole } from '../users/entities/user.entity';
import { ParseUuidOr404Pipe } from '../common/pipes/parse-uuid-or-404.pipe';
import { CurrentUser } from '../common/decorators/current-user.decorator';
@Controller('posts')
export class PostsController {
constructor(private postsService: PostsService) {}
@Get()
findAll(@Query('page') page = 1, @Query('limit') limit = 10) {
return this.postsService.findAll(Number(page), Number(limit));
}
@Get(':id')
findOne(@Param('id', ParseUuidOr404Pipe) id: string) {
return this.postsService.findOne(id);
}
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Roles(UserRole.AUTHOR, UserRole.ADMIN)
@Post()
create(@Body() dto: CreatePostDto, @CurrentUser() user) {
return this.postsService.create(dto, user.userId);
}
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Roles(UserRole.AUTHOR, UserRole.ADMIN)
@Patch(':id')
update(@Param('id', ParseUuidOr404Pipe) id: string, @Body() dto: UpdatePostDto, @CurrentUser() user) {
return this.postsService.update(id, dto, user.userId, user.role);
}
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Roles(UserRole.ADMIN)
@Delete(':id')
remove(@Param('id', ParseUuidOr404Pipe) id: string) {
return this.postsService.remove(id);
}
@UseGuards(AuthGuard('jwt'))
@Post(':id/cover')
@UseInterceptors(FileInterceptor('file', {
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
fileFilter: (_req, file, cb) => {
const allowed = ['image/jpeg', 'image/png', 'image/webp'];
cb(null, allowed.includes(file.mimetype));
},
}))
uploadCover(@Param('id', ParseUuidOr404Pipe) id: string, @UploadedFile() file: Express.Multer.File) {
return this.postsService.setCoverImage(id, file);
}
}
Beachte, wie jede Route nur die strikt notwendigen Guards und Rollen
anwendet: findAll/findOne sind öffentlich (kein Guard),
create/update erfordern die Rolle author oder
admin, während remove nur admin vorbehalten ist — ein
konkretes Beispiel für das Prinzip der geringsten Rechte, angewendet auf
jede einzelne Route.
Posts Service
// src/posts/posts.service.ts
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Post } from './entities/post.entity';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { UserRole } from '../users/entities/user.entity';
import slugify from 'slugify';
@Injectable()
export class PostsService {
constructor(@InjectRepository(Post) private postsRepo: Repository) {}
async findAll(page: number, limit: number) {
const [items, total] = await this.postsRepo.findAndCount({
where: { published: true },
order: { createdAt: 'DESC' },
skip: (page - 1) * limit,
take: limit,
});
return { items, total, page, limit };
}
async findOne(id: string): Promise {
const post = await this.postsRepo.findOne({ where: { id } });
if (!post) throw new NotFoundException(`Post ${id} non trovato`);
return post;
}
async create(dto: CreatePostDto, authorId: string): Promise {
const post = this.postsRepo.create({
...dto,
slug: slugify(dto.title, { lower: true, strict: true }),
author: { id: authorId } as any,
});
return this.postsRepo.save(post);
}
async update(id: string, dto: UpdatePostDto, userId: string, userRole: UserRole): Promise {
const post = await this.findOne(id);
// Ein Author darf nur seine eigenen Posts bearbeiten; der Admin darf alle bearbeiten
if (userRole !== UserRole.ADMIN && post.author.id !== userId) {
throw new ForbiddenException('Non puoi modificare un post di un altro autore');
}
Object.assign(post, dto);
return this.postsRepo.save(post);
}
async remove(id: string): Promise {
const result = await this.postsRepo.delete(id);
if (result.affected === 0) throw new NotFoundException(`Post ${id} non trovato`);
}
async setCoverImage(id: string, file: Express.Multer.File): Promise {
const post = await this.findOne(id);
// In Produktion: Upload zu S3/Cloud Storage statt lokalem Datenträger
post.coverImageUrl = `/uploads/${file.filename}`;
return this.postsRepo.save(post);
}
}
Beachte die Prüfung userRole !== UserRole.ADMIN && post.author.id !== userId
in der Methode update: Es ist ein Beispiel für Autorisierung auf
Ressourcenebene (nicht nur auf Routenebene) — ein Author, der mit einem gültigen JWT
authentifiziert ist, kann dennoch blockiert werden, wenn er versucht, die Ressource einer
anderen Person zu ändern. Diese Prüfung muss immer im Service erfolgen, niemals allein dem
Guard überlassen werden, der auf einer zu generischen Ebene arbeitet, um den Eigentümer einer
bestimmten Ressource zu kennen.
Best Practices
| Prinzip | Praktische Anwendung in NestJS |
|---|---|
| Clean Architecture | Klare Trennung zwischen Controller (HTTP), Service (Geschäftslogik) und Repository (Persistenz). Die Domäne sollte niemals von Infrastrukturdetails wie Express oder TypeORM abhängen. |
| SOLID — Single Responsibility | Ein Controller mappt HTTP-Requests, ein Service enthält einen einzigen Bereich der Geschäftslogik, ein Repository ein einziges Aggregate. |
| SOLID — Dependency Inversion | Services hängen von abstrakten Interfaces/Tokens ab (z. B. @InjectRepository), nicht von konkreten Implementierungen — ersetzbar durch Mocks in Tests. |
| DRY | Gemeinsame Validierungslogik in DTOs mit PartialType/PickType/OmitType, um Duplikation zwischen Create- und Update-DTO zu vermeiden. |
| KISS | Vermeide die Einführung von CQRS, Event Sourcing oder Microservices, bis die reale Komplexität der Domäne dies rechtfertigt. |
| Repository Pattern | TypeORM/Prisma bieten diese Schicht bereits; vermeide es, die Abstraktion zu "durchlöchern", indem du außer in performance-kritischen Fällen rohe SQL-Abfragen direkt in Services aufrufst. |
| DTO | Jeder Endpunkt hat ein dediziertes Input-DTO — niemals Datenbank-Entities direkt als API-Vertrag offenlegen. |
| Validation | Globale ValidationPipe mit whitelist und forbidNonWhitelisted immer aktiv in jedem Projekt, ohne Ausnahmen. |
Performance
Caching
// Endpoint-Level-Caching mit @nestjs/cache-manager
import { CacheInterceptor, CacheTTL } from '@nestjs/cache-manager';
import { UseInterceptors } from '@nestjs/common';
@UseInterceptors(CacheInterceptor)
@CacheTTL(60) // Cache für 60 Sekunden
@Get()
findAll() {
return this.postsService.findAll(1, 10);
}
Lazy Loading von Modulen
In großen monolithischen Anwendungen erlaubt LazyModuleLoader, selten genutzte
Module (z. B. ein Reporting-Modul) erst dann zu laden, wenn sie tatsächlich benötigt werden,
was die Bootstrap-Zeit reduziert.
Optimierte Queries
- Verwende explizites
selectin TypeORM, um die Übertragung unnötiger Spalten zu vermeiden (z. B.contentin Listenansichten ausschließen). - Füge Indizes auf Spalten hinzu, die häufig in
WHERE/ORDER BYverwendet werden (siehe@Indexauf dem Slug in derPost-Entity). - Vermeide das N+1-Problem: verwende
relationsoderQueryBuildermitleftJoinAndSelectstatt Relationen innerhalb einer Schleife zu laden. - Verwende immer Paginierung (
skip/take) — gib niemals unbegrenzte Collections zurück.
Logging und Monitoring
Ersetze in der Produktion den Standard-Logger durch nestjs-pino für
strukturierte JSON-Logs, die von Tools wie Grafana Loki oder Datadog leicht indexiert werden
können. Integriere für das Performance-Monitoring @nestjs/terminus für Health
Checks (/health), die von Load Balancern und Orchestratoren (Kubernetes,
Railway) verwendet werden.
Profiling
Um echte Engpässe zu finden, verwende node --prof oder APM-Tools (Application
Performance Monitoring) wie New Relic oder Elastic APM, die die Zeit in jedem Handler, jeder
Query und jedem externen Aufruf verfolgen — vermeide Optimierung "nach Gefühl" ohne echte
Daten.
Sicherheit
| Maßnahme | Umsetzung |
|---|---|
| JWT | Kurzlebiger Access Token, signiert mit einem robusten Secret (32+ zufällige Zeichen), niemals im Code hardcodiert. |
| Refresh Token | Längere Lebensdauer, idealerweise widerrufbar (Whitelist in Redis/DB), Rotation bei jeder Verwendung. |
| Passwort-Hashing / bcrypt | bcrypt.hash(password, 12) — niemals MD5/SHA1, niemals Klartext-Passwörter in der DB. |
| Helmet | app.use(helmet()) setzt Sicherheits-Header (CSP, X-Frame-Options, HSTS) in einer einzigen Zeile. |
| CORS | app.enableCors({ origin: [...] }) mit expliziter Domain-Whitelist, niemals origin: '*' in Produktion, wenn die API Credentials erfordert. |
| Rate Limiting | @nestjs/throttler, um Requests pro IP zu begrenzen, essenziell bei Endpunkten wie /auth/login, um Brute-Force-Angriffe abzuschwächen. |
| ValidationPipe | whitelist + forbidNonWhitelisted, um Mass Assignment bei unerwarteten Feldern zu blockieren. |
| Input-Sanitisierung | class-validator für die Struktur, plus explizite HTML-Sanitisierung (z. B. sanitize-html), wenn ein Feld freies Markup erlaubt, um XSS zu verhindern. |
// Beispiel: aggressiveres Rate Limiting speziell für den Login
import { Throttle } from '@nestjs/throttler';
@Throttle({ default: { limit: 5, ttl: 60000 } }) // max. 5 Versuche pro Minute und IP
@Post('login')
login(@Body() dto: LoginDto) {
return this.authService.login(dto);
}
Häufige Fehler
1. whitelist/forbidNonWhitelisted in ValidationPipe Vergessen
Problem: ein Client kann zusätzliche, vom DTO nicht vorgesehene Felder senden (z. B. role: 'admin').
Ursache: ValidationPipe ohne whitelist: true konfiguriert.
Lösung: aktiviere whitelist und forbidNonWhitelisted immer global in main.ts.
2. synchronize: true in Produktion
Problem: TypeORM kann zur Laufzeit Spalten oder Tabellen ändern/löschen.
Ursache: Verwechslung zwischen Entwicklungs- und Produktionsumgebung bei der Datenbankkonfiguration.
Lösung: synchronize: false in Produktion, verwalte das Schema mit expliziten Migrationen (typeorm migration:generate).
3. Zirkuläre Abhängigkeit Zwischen Modulen
Problem: Fehler "Nest cannot resolve dependencies" oder ein stiller Absturz beim Start.
Ursache: zwei Module importieren sich direkt gegenseitig.
Lösung: verwende forwardRef(() => ModuleB) in beiden Modulen, oder — besser — extrahiere die gemeinsame Abhängigkeit in ein drittes Modul.
4. Entities Direkt als API-Response Offenlegen
Problem: sensible Felder (z. B. passwordHash) landen in der JSON-Response.
Ursache: der Controller gibt die TypeORM-Entity unverändert zurück, ohne eine DTO-/Serializer-Schicht.
Lösung: verwende @Exclude() von class-transformer auf der Entity zusammen mit einem globalen ClassSerializerInterceptor, oder besser noch ein explizites Response-DTO.
5. Passwörter Ohne Hashing Gespeichert
Problem: eine Kompromittierung der Datenbank legt alle Passwörter im Klartext offen.
Ursache: Auslassung (oft während des Rapid Prototyping) des Hashing-Schritts.
Lösung: Hashing mit bcrypt/argon2 immer, auch in der Entwicklung — es gibt nie einen gültigen Grund, es auszulassen.
6. Schwache oder Hardcodierte JWT-Secrets
Problem: ein vorhersehbares Secret erlaubt die Fälschung gültiger Tokens.
Ursache: ein Standard-Secret, das in der Produktion belassen oder ins Repository committet wurde.
Lösung: zufällig generierte Secrets (mindestens 256 Bit), ausschließlich über Umgebungsvariablen/einen Secret Manager verwaltet, niemals im Quellcode.
7. Fehlende Behandlung Asynchroner Fehler in Services
Problem: unbehandelte Promise Rejections führen zum Absturz des Node-Prozesses.
Ursache: async-Aufrufe ohne try/catch, wo nötig, oder das Vergessen des globalen Exception Filters.
Lösung: lass Exceptions bis zum globalen Exception Filter aufsteigen (Standardverhalten von Nest), fange nur ab, wo ein anderes Verhalten benötigt wird (Fallback, Retry).
8. Unentdeckte N+1-Queries
Problem: der Endpunkt wird mit wachsenden Daten extrem langsam.
Ursache: Laden von Relationen innerhalb einer Schleife statt eines einzigen Joins.
Lösung: verwende relations/leftJoinAndSelect, überwache die generierten Queries mit logging: true in der Entwicklung.
9. Offener CORS für Alle in Produktion mit Credentials
Problem: jede Website kann im Namen des Nutzers authentifizierte Requests an die API senden.
Ursache: origin: '*' kombiniert mit credentials: true.
Lösung: explizite Whitelist autorisierter Domains.
10. Kein Rate Limiting bei Authentifizierungs-Endpunkten
Problem: Brute-Force-Angriffe auf Login/Registrierung.
Ursache: @nestjs/throttler nicht angewendet, oder mit zu großzügigen Schwellenwerten.
Lösung: ein restriktiveres @Throttle speziell für Login/Register/Passwort-Reset.
11. Update-DTO Identisch mit Create-DTO Ohne PartialType
Problem: jeder PATCH erfordert alle Pflichtfelder, was partielle Updates unbrauchbar macht.
Ursache: das Create-DTO wird kopiert, ohne die Felder optional zu machen.
Lösung: class UpdateXDto extends PartialType(CreateXDto) {}.
12. Guards Nur auf Routenebene Angewendet, Nie auf Ressourcenebene
Problem: ein authentifizierter Nutzer kann Ressourcen anderer Nutzer ändern.
Ursache: Verlass allein auf @UseGuards(AuthGuard('jwt')) ohne Ownership-Prüfung im Service.
Lösung: explizite Prüfung resource.ownerId === user.id (oder Admin-Rolle) im Service, wie im Beispiel PostsService.update gezeigt.
13. Datenbankverbindung Ohne Korrekt Konfigurierten Pool
Problem: Erschöpfung der Verbindungen unter Last, Fehler "too many clients".
Ursache: Standardwerte des Pools, die nicht zum realen Traffic passen.
Lösung: konfiguriere explizit extra: { max: 20 } in der TypeORM-Connection, kalibriert auf den Datenbankplan.
14. Datei-Uploads Ohne Größen- oder Typbeschränkungen
Problem: ein böswilliger Nutzer lädt riesige Dateien oder als Bilder getarnte ausführbare Dateien hoch.
Ursache: FileInterceptor ohne limits oder fileFilter konfiguriert.
Lösung: setze immer limits.fileSize und validiere den mimetype, wie im Beispiel uploadCover.
15. Beim Start Nicht Validierte Umgebungsvariablen
Problem: die App startet "stillschweigend" mit unvollständiger Konfiguration und schlägt später kryptisch fehl.
Ursache: kein Validierungsschema für die Env Vars.
Lösung: verwende ConfigModule.forRoot({ validationSchema: Joi.object({...}) }), damit der Bootstrap sofort fehlschlägt, wenn eine kritische Variable fehlt.
16. Logging Sensibler Daten
Problem: Passwörter, Tokens oder personenbezogene Daten landen im Klartext in den Logs.
Ursache: wahlloses Logging des gesamten Request-Bodys.
Lösung: redaktiere (maskiere) sensible Felder explizit vor dem Logging, oder verwende die Redact Paths von pino.
17. Tests Nur für den "Happy Path" Geschrieben
Problem: Bugs, die nur bei ungültigem Input oder Edge Cases auftreten, bleiben bis zur Produktion unsichtbar.
Ursache: Zeitdruck, Testabdeckung auf den Hauptablauf beschränkt.
Lösung: teste explizit die erwarteten 4xx-Fehler (Validierung, Berechtigungen, Not Found), nicht nur die 2xx.
18. Fehlende API-Versionierung
Problem: jede Breaking Change bricht bestehende Clients ohne Vorwarnung.
Ursache: kein Versionspräfix (/api/v1) oder konfigurierte Nest-Versionierungsstrategie.
Lösung: app.setGlobalPrefix('api/v1') von Tag eins an, auch bei kleinen Projekten.
19. Veraltete Abhängigkeiten mit Bekannten Sicherheitslücken
Problem: die API bleibt öffentlich dokumentierten CVEs ausgesetzt.
Ursache: fehlender Prozess für periodische Dependency-Audits.
Lösung: npm audit in die CI integriert, regelmäßige Updates mit Dependabot/Renovate.
20. Keine Unterscheidung Zwischen Validierungs- und Domänenfehlern
Problem: alle Fehler kommen als generischer 500 zurück, wodurch der Client eine fehlerhafte Eingabe nicht von einem Serverproblem unterscheiden kann.
Ursache: Verwendung von generischem Error statt der spezifischen HttpException-Klassen von Nest.
Lösung: verwende immer die korrekten typisierten Exceptions (BadRequestException, NotFoundException, ForbiddenException, ConflictException...) — so kommuniziert Nest den korrekten HTTP-Statuscode.
FAQ
1. Eignet sich NestJS auch für kleine Projekte?
Ja, aber der wahre Wert zeigt sich, wenn das Projekt wächst: bei einem kleinen Skript kann die Struktur im Vergleich zu reinem Express übertrieben wirken.
2. Muss ich zwingend TypeScript verwenden?
Technisch unterstützt NestJS auch reines JavaScript, aber man verliert einen Großteil des Werts (typisierte Decorators, type-safe DI). In der Produktion wird dringend davon abgeraten.
3. TypeORM oder Prisma?
TypeORM integriert sich nativer mit den NestJS-Decorators und ist im Nest-Ökosystem ausgereifter; Prisma bietet einen type-saferen generierten Client und eine bessere Developer Experience bei Migrationen, erfordert aber eine manuelle Integrationsschicht mit Nest.
4. Wie verwalte ich Datenbank-Migrationen?
Mit TypeORM: typeorm migration:generate, um sie aus dem Entity-Diff zu generieren, typeorm migration:run, um sie in CI/CD anzuwenden, niemals synchronize: true in der Produktion.
5. Wie strukturiere ich ein sehr großes Projekt?
Modularisiere nach Domäne (Feature Modules), nicht nach technischem Typ — vermeide globale Ordner "controllers/", "services/", die verschiedene Domänen vermischen.
6. Wie teste ich Controller?
Mit Test.createTestingModule von @nestjs/testing, indem die injizierten Services gemockt werden; für Services mocke die TypeORM-Repositories mit getRepositoryToken.
7. Was ist der Unterschied zwischen Guard und Middleware für die Authentifizierung?
Middleware hat keinen Zugriff auf den Ausführungskontext von Nest (z. B. Decorator-Metadaten), kann also @Roles() nicht lesen. Guards können das — deshalb gehört Authentifizierung/Autorisierung immer in Guards, nie in Middleware.
8. Kann ich GraphQL statt REST verwenden?
Ja, NestJS hat offizielle Unterstützung sowohl für Apollo Server als auch für Mercurius, mit demselben Modul-/DI-System — nützlich, wenn du flexible clientseitige Queries benötigst.
9. Wie verwalte ich Datenbanktransaktionen?
Mit DataSource.transaction() von TypeORM, oder mit dem @Transactional()-Decorator der Bibliothek typeorm-transactional für eine deklarativere Syntax.
10. Wie implementiere ich Refresh Tokens sicher?
Speichere einen Hash des Refresh Tokens serverseitig (Whitelist), rotiere ihn bei jeder Verwendung (Token Rotation) und invalidiere ihn explizit beim Logout.
11. Unterstützt NestJS Microservices?
Ja, nativ, mit Transport Layers für TCP, Redis, RabbitMQ, Kafka, gRPC und NATS über @nestjs/microservices.
12. Wie deploye ich in die Produktion?
Build mit nest build (kompiliert nach dist/), dann node dist/main.js; auf Plattformen wie Railway/Render/Fly.io genügt ein mehrstufiges Dockerfile oder der native Node-Buildpack.
13. Wie verwalte ich unterschiedliche Umgebungsvariablen für dev/staging/prod?
ConfigModule.forRoot({ envFilePath: ['.env.${NODE_ENV}', '.env'] }), wobei die echten Werte in Produktion von der Hosting-Plattform injiziert werden, nicht aus committeten .env-Dateien.
14. Wie implementiere ich Uploads zu S3 statt lokalem Datenträger?
Ersetze die Storage Engine von Multer durch multer-s3, oder verwalte den Upload manuell im Service mit dem AWS SDK, nachdem der Buffer im Speicher empfangen wurde.
15. Wie dokumentiere ich die API automatisch?
Mit @nestjs/swagger: die Decorators @ApiTags, @ApiProperty auf DTOs und SwaggerModule.setup() in main.ts generieren eine interaktive OpenAPI-UI unter /api/docs.
16. Wie implementiere ich Soft Delete?
TypeORM unterstützt nativ @DeleteDateColumn() auf der Entity: softDelete() setzt die Spalte, statt die Zeile zu entfernen, und Queries schließen gelöschte Datensätze automatisch aus.
17. Wie verwalte ich mehrere API-Versionen gleichzeitig?
Mit app.enableVersioning({ type: VersioningType.URI }) und dem @Version('2')-Decorator auf den einzelnen Controllern/Methoden, die mit v1 koexistieren müssen.
18. Ist ein ORM notwendig, oder kann ich rohe SQL-Queries verwenden?
In den meisten Fällen reduziert ein ORM Fehler und Boilerplate; für sehr komplexe oder performance-kritische Queries erlaubt TypeORM dennoch rohe Queries über QueryRunner, während der Rest der App auf dem ORM bleibt.
19. Wie implementiere ich WebSocket in NestJS?
Mit @nestjs/websockets und einem @WebSocketGateway(), das sich mit Socket.IO oder ws integriert und dabei dasselbe DI- und Guard-System wie REST-Routen beibehält.
20. Wie verwalte ich Cron Jobs?
Mit @nestjs/schedule und dem @Cron('0 0 * * *')-Decorator auf einer Service-Methode — nützlich für Datenbereinigung, Versand von Digest-E-Mails, periodische Synchronisationen.
21. Wie schütze ich die API vor SQL-Injection-Angriffen?
TypeORM/Prisma parametrisieren Queries automatisch — das Risiko entsteht nur, wenn du rohe Queries durch manuelles Konkatenieren von Strings erstellst, was immer vermieden werden sollte.
22. Was ist die korrekte Art, Secrets in der Produktion zu verwalten?
Niemals im Code oder in committeten Dateien: verwende die Umgebungsvariablen der Hosting-Plattform oder einen dedizierten Secret Manager (AWS Secrets Manager, Doppler, Railway Variables).
23. Wie skaliere ich eine NestJS-API horizontal?
Die App muss zustandslos sein (keine lokalen In-Memory-Sessions — verwende JWT oder Redis für gemeinsamen Zustand), wird dann hinter einem Load Balancer repliziert; für WebSocket wird ein Redis-Adapter benötigt, um Verbindungen zwischen Instanzen zu synchronisieren.
24. Wie handhabe ich Abwärtskompatibilität, wenn ich ein Response-Schema ändere?
Führe ein neues Feld ein, statt das bestehende umzubenennen, deprekiere es schrittweise mit klarer Dokumentation, und verwende API-Versionierung für wirklich Breaking Changes.
25. Lohnt es sich, zusätzlich zu Unit Tests E2E-Tests für eine NestJS-API zu schreiben?
Ja — @nestjs/testing mit supertest erlaubt es, die gesamte Pipeline (inklusive Guards, Pipes, Interceptors) zu testen, indem echte Endpunkte auf einer In-Memory-App-Instanz aufgerufen werden, wodurch Integrationsprobleme erfasst werden, die Unit Tests allein nicht sehen.
Ressourcen und Offizielle Dokumentation
Um jedes in diesem Guide behandelte Thema zu vertiefen, bleibt die offizielle Dokumentation stets die zuverlässigste und aktuellste Quelle:
- NestJS — offizielle Dokumentation:
docs.nestjs.com, insbesondere die Abschnitte zu Guards, Interceptors, Pipes und Exception Filters. - TypeORM — offizielle Dokumentation:
typeorm.io, für Relationen, Migrationen und den erweiterten Query Builder. - Passport.js:
passportjs.org, die Authentifizierungsbibliothek, auf der@nestjs/passportbasiert. - class-validator: offizielles GitHub-Repository, mit der vollständigen Liste der verfügbaren Validierungs-Decorators über die in diesem Guide verwendeten hinaus.
- OWASP API Security Top 10: die Referenz-Checkliste für die Sicherheit von REST-APIs, ergänzend zum Sicherheitsabschnitt dieses Guides.
- jwt.io: ein Tool zum Inspizieren und Dekodieren von JWT-Tokens beim Debuggen der Authentifizierung.
Empfohlene Verwandte Artikel
Dieser Guide deckt den gesamten Lebenszyklus einer NestJS-REST-API ab, aber jeder Abschnitt kann zu einer eigenen Vertiefung werden. Hier die natürlich verwandten Themen, die es sich lohnt, als Nächstes zu erkunden:
- Modularisierung eines großen NestJS-Projekts: Feature Modules, Shared Modules und Barrel Files.
- Dependency Injection in NestJS mit Beispielen erklärt: Custom Provider, Factory Provider, Injection Tokens.
- JWT-Authentifizierung und Refresh Tokens im Detail: Token Rotation, Widerruf und Multi-Geräte-Verwaltung.
- Fortgeschrittenes Role Based Access Control (RBAC): granulare Berechtigungen über einfache Rollen hinaus.
- Validierung mit ValidationPipe und class-validator: Custom Validators und lokalisierte Fehlermeldungen.
- Zentralisiertes Logging in NestJS mit Pino und Request-Korrelation über Request IDs.
- Custom Exception Filters für spezifische Domänen (z. B. Zahlungsfehler, Domänenfehler).
- Fortgeschrittene Datei-Uploads mit Multer: S3-Speicherung, Validierung des tatsächlichen Dateiinhalts.
- Scheduler und Cron Jobs in NestJS mit
@nestjs/schedule. - WebSocket mit NestJS: Echtzeit-Benachrichtigungen und Skalierung mit dem Redis-Adapter.
- Testing von NestJS-Controllern und -Services: Unit Tests, Mocking von Repositories, E2E-Tests mit Supertest.
Fazit
Eine REST API mit NestJS zu bauen bedeutet, in eine Struktur zu investieren, die mit der Komplexität des Projekts skaliert: gut isolierte Module, Dependency Injection für Testbarkeit, Guards/Interceptors/Pipes zur Trennung von Cross-Cutting Concerns von der Geschäftslogik, und ein Ökosystem — TypeORM, Passport, class-validator, Swagger — das jeden realen Produktionsbedarf ausgereift abdeckt. Das in diesem Guide gebaute Blog-API-Beispiel (CRUD, JWT mit Refresh Token, RBAC, Datei-Upload, zentralisiertes Logging, Fehlerbehandlung) ist das wiederverwendbare Skelett für die meisten NestJS-APIs, denen du in der Praxis begegnen wirst.
Empfohlene nächste Schritte: vertiefe die Modularisierung großer NestJS-Projekte, JWT-Authentifizierung und Refresh Tokens im Detail, das RBAC-Pattern mit granularen Berechtigungen, und Testing-Techniken für Controller und Services — Themen, die diesen Guide natürlich ergänzen.