<link rel="stylesheet" href="/assets/fonts/jetbrains-mono/jetbrains-mono.css" />
All posts

Eine vollständige REST API mit Nestjs erstellen: Der definitive leitfaden zu Typeorm, JWT, RBAC und sicherheit

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

VorteileNachteile
Klare, konsistente Struktur über verschiedene Projekte hinwegSteilere Lernkurve als reines Express (Decorators, DI, Module)
Native Testbarkeit dank DIBoilerplate-Overhead bei sehr kleinen Projekten
Breites, gut gepflegtes offizielles ÖkosystemMehr "Magie" (Decorators, Reflection) im Vergleich zu explizitem Code
Hervorragende Integration mit TypeScript und Swagger/OpenAPIEtwas höhere Bundle-Größe und Cold-Start als minimalistische Frameworks (relevant in serverlosen Umgebungen)
Erleichtert die Einführung von Clean/Hexagonal ArchitectureErfordert 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.

ToolEmpfohlene VersionZweck
Node.js20.x LTS oder höherJavaScript-Runtime, auf der NestJS läuft
npm10.x (in Node 20 enthalten)Paketverwaltung
NestJS CLI@nestjs/cli 10.x+Scaffolding von Modulen, Controllern, Services
TypeScript5.xSprache, in der NestJS und deine App geschrieben sind
VS CodeNeueste stabile VersionEditor mit nativer TypeScript-Unterstützung
PostgreSQL15.x oder höherRelationale 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

PrinzipPraktische Anwendung in NestJS
Clean ArchitectureKlare 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 ResponsibilityEin Controller mappt HTTP-Requests, ein Service enthält einen einzigen Bereich der Geschäftslogik, ein Repository ein einziges Aggregate.
SOLID — Dependency InversionServices hängen von abstrakten Interfaces/Tokens ab (z. B. @InjectRepository), nicht von konkreten Implementierungen — ersetzbar durch Mocks in Tests.
DRYGemeinsame Validierungslogik in DTOs mit PartialType/PickType/OmitType, um Duplikation zwischen Create- und Update-DTO zu vermeiden.
KISSVermeide die Einführung von CQRS, Event Sourcing oder Microservices, bis die reale Komplexität der Domäne dies rechtfertigt.
Repository PatternTypeORM/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.
DTOJeder Endpunkt hat ein dediziertes Input-DTO — niemals Datenbank-Entities direkt als API-Vertrag offenlegen.
ValidationGlobale 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 select in TypeORM, um die Übertragung unnötiger Spalten zu vermeiden (z. B. content in Listenansichten ausschließen).
  • Füge Indizes auf Spalten hinzu, die häufig in WHERE/ORDER BY verwendet werden (siehe @Index auf dem Slug in der Post-Entity).
  • Vermeide das N+1-Problem: verwende relations oder QueryBuilder mit leftJoinAndSelect statt 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ßnahmeUmsetzung
JWTKurzlebiger Access Token, signiert mit einem robusten Secret (32+ zufällige Zeichen), niemals im Code hardcodiert.
Refresh TokenLängere Lebensdauer, idealerweise widerrufbar (Whitelist in Redis/DB), Rotation bei jeder Verwendung.
Passwort-Hashing / bcryptbcrypt.hash(password, 12) — niemals MD5/SHA1, niemals Klartext-Passwörter in der DB.
Helmetapp.use(helmet()) setzt Sicherheits-Header (CSP, X-Frame-Options, HSTS) in einer einzigen Zeile.
CORSapp.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.
ValidationPipewhitelist + forbidNonWhitelisted, um Mass Assignment bei unerwarteten Feldern zu blockieren.
Input-Sanitisierungclass-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/passport basiert.
  • 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.

💬 Leser-Notizen

0 Notizen

Notiz schreiben

Teile deine Meinung, einen Vorschlag oder ein Kompliment

Neueste Notizen

Noch keine Notizen. Sei der Erste, der kommentiert!