# NestJS: Eine vollstaendige REST-API von Grund auf erstellen > Schritt-fuer-Schritt-Anleitung zum Erstellen einer produktionsreifen REST-API mit NestJS, TypeScript, Prisma und class-validator. CRUD, Validierung, Fehlerbehandlung und Interceptors. - Published: 2026-01-13 - Updated: 2026-04-07 - Author: SharpSkill - Tags: nestjs, nodejs, typescript, rest api, backend - Reading time: 15 min --- NestJS hat sich als das fuehrende Node.js-Framework fuer den Aufbau skalierbarer serverseitiger Anwendungen etabliert. Die modulare Architektur, das integrierte Dependency-Injection-System und die vollstaendige TypeScript-Unterstuetzung machen NestJS zur idealen Wahl fuer professionelle REST-APIs. Mit NestJS 11 kommen weitere Verbesserungen hinzu, die Entwicklungsgeschwindigkeit und Laufzeitperformance nochmals steigern. > **Warum NestJS im Jahr 2026?** > > NestJS 11 bringt optimiertes Modul-Scanning, schnellere Startup-Zeiten und verbesserte Decorators. Das Framework kombiniert die besten Konzepte aus Angular (Decorators, Module, DI) mit der Flexibilitaet von Node.js und bietet damit eine erstklassige Entwicklererfahrung fuer Backend-Projekte. ## Projektinstallation und Konfiguration Die NestJS CLI erstellt ein vorkonfiguriertes Projekt mit TypeScript, ESLint und einer durchdachten Ordnerstruktur. Der Einstieg erfordert nur wenige Befehle. ```bash # terminal # Global installation of the NestJS CLI npm install -g @nestjs/cli # Create a new project nest new my-api # Navigate to the project cd my-api # Start in development mode with hot-reload npm run start:dev ``` Die Datei `main.ts` dient als Einstiegspunkt der Anwendung. Hier werden globale Pipes, Praefix und weitere Konfigurationen festgelegt. ```typescript // src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { // Create the NestJS application const app = await NestFactory.create(AppModule); // Enable global validation app.useGlobalPipes(new ValidationPipe({ whitelist: true, // Removes non-decorated properties forbidNonWhitelisted: true, // Rejects requests with unknown properties transform: true, // Transforms payloads to DTO instances })); // Configure global prefix for all routes app.setGlobalPrefix('api'); await app.listen(3000); } bootstrap(); ``` Die `ValidationPipe` mit `whitelist: true` entfernt automatisch Felder, die nicht im DTO definiert sind. Mit `forbidNonWhitelisted: true` wird sogar ein Fehler geworfen, wenn unbekannte Felder gesendet werden. Das schuetzt die API vor unerwuenschten Daten. ## Modulare Architektur verstehen NestJS organisiert Code in Modulen. Jedes Modul kapselt zusammengehoerige Controller, Services und Entitaeten. Das Root-Modul importiert alle Feature-Module und bildet den Ausgangspunkt der Anwendung. ```typescript // src/app.module.ts import { Module } from '@nestjs/common'; import { UsersModule } from './users/users.module'; import { ProductsModule } from './products/products.module'; import { AuthModule } from './auth/auth.module'; // The root module imports all application modules @Module({ imports: [ UsersModule, // User management ProductsModule, // Product catalog AuthModule, // Authentication ], }) export class AppModule {} ``` Jedes Feature-Modul folgt demselben Muster: Controller fuer HTTP-Anfragen, Services fuer Geschaeftslogik und Exports fuer moduluebergreifende Nutzung. ```typescript // src/users/users.module.ts import { Module } from '@nestjs/common'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; @Module({ // Controllers handle HTTP requests controllers: [UsersController], // Providers are injectable throughout the module providers: [UsersService], // Exports make providers available to other modules exports: [UsersService], }) export class UsersModule {} ``` Diese Struktur erzwingt eine klare Trennung der Verantwortlichkeiten. Der `UsersService` wird ueber Dependency Injection in den Controller eingebunden. Durch den `exports`-Array kann er auch in anderen Modulen genutzt werden, etwa im `AuthModule`. ## Einen vollstaendigen CRUD-Controller erstellen Der Controller definiert die HTTP-Endpunkte und delegiert die Geschaeftslogik an den Service. NestJS-Decorators wie `@Get()`, `@Post()`, `@Put()` und `@Delete()` mappen die Methoden auf HTTP-Verben. ```typescript // src/users/users.controller.ts import { Controller, Get, Post, Put, Delete, Body, Param, Query, HttpCode, HttpStatus, ParseIntPipe, } from '@nestjs/common'; import { UsersService } from './users.service'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { User } from './entities/user.entity'; // Route prefix: /api/users @Controller('users') export class UsersController { // Inject service via constructor constructor(private readonly usersService: UsersService) {} // POST /api/users - Create a user @Post() @HttpCode(HttpStatus.CREATED) async create(@Body() createUserDto: CreateUserDto): Promise { // The DTO is automatically validated before reaching here return this.usersService.create(createUserDto); } // GET /api/users - List with pagination @Get() async findAll( @Query('page', new ParseIntPipe({ optional: true })) page: number = 1, @Query('limit', new ParseIntPipe({ optional: true })) limit: number = 10, ): Promise<{ data: User[]; total: number }> { return this.usersService.findAll(page, limit); } // GET /api/users/:id - Retrieve by ID @Get(':id') async findOne(@Param('id', ParseIntPipe) id: number): Promise { // ParseIntPipe automatically converts and validates the parameter return this.usersService.findOne(id); } // PUT /api/users/:id - Full update @Put(':id') async update( @Param('id', ParseIntPipe) id: number, @Body() updateUserDto: UpdateUserDto, ): Promise { return this.usersService.update(id, updateUserDto); } // DELETE /api/users/:id - Deletion @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) async remove(@Param('id', ParseIntPipe) id: number): Promise { await this.usersService.remove(id); } } ``` > **Eingebaute Validation Pipes** > > Die `ParseIntPipe` konvertiert String-Parameter automatisch in Zahlen und gibt einen 400-Fehler zurueck, falls der Wert keine gueltige Zahl ist. NestJS bietet weitere eingebaute Pipes wie `ParseBoolPipe`, `ParseArrayPipe` und `ParseUUIDPipe` fuer gaengige Konvertierungen. ## Implementierung des Business-Service Der Service enthaelt die gesamte Geschaeftslogik und ist durch den `@Injectable()`-Decorator als Provider markiert. In dieser ersten Version wird ein In-Memory-Array als Datenspeicher verwendet, bevor spaeter die Integration mit Prisma folgt. ```typescript // src/users/users.service.ts import { Injectable, NotFoundException } from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { User } from './entities/user.entity'; @Injectable() export class UsersService { // In-memory database simulation private users: User[] = []; private idCounter = 1; async create(createUserDto: CreateUserDto): Promise { // Create the user entity const user: User = { id: this.idCounter++, ...createUserDto, createdAt: new Date(), updatedAt: new Date(), }; this.users.push(user); return user; } async findAll(page: number, limit: number): Promise<{ data: User[]; total: number }> { // Calculate pagination const start = (page - 1) * limit; const end = start + limit; return { data: this.users.slice(start, end), total: this.users.length, }; } async findOne(id: number): Promise { const user = this.users.find(u => u.id === id); // Throw exception if user doesn't exist if (!user) { throw new NotFoundException(`User with ID ${id} not found`); } return user; } async update(id: number, updateUserDto: UpdateUserDto): Promise { const user = await this.findOne(id); // Merge existing data with updates Object.assign(user, updateUserDto, { updatedAt: new Date() }); return user; } async remove(id: number): Promise { const index = this.users.findIndex(u => u.id === id); if (index === -1) { throw new NotFoundException(`User with ID ${id} not found`); } this.users.splice(index, 1); } // Utility method for other services async findByEmail(email: string): Promise { return this.users.find(u => u.email === email); } } ``` Der Service nutzt `NotFoundException` aus `@nestjs/common`, um standardkonforme 404-Fehler auszuloesen. NestJS wandelt diese Exceptions automatisch in passende HTTP-Antworten um. ## Datenvalidierung mit class-validator DTOs (Data Transfer Objects) definieren die Struktur eingehender Daten und nutzen Decorators von `class-validator` fuer die Validierung. Das `CreateUserDto` legt fest, welche Felder beim Erstellen eines Benutzers erforderlich sind. ```typescript // src/users/dto/create-user.dto.ts import { IsEmail, IsNotEmpty, IsString, MinLength, MaxLength, IsOptional, Matches, } from 'class-validator'; export class CreateUserDto { @IsNotEmpty({ message: 'Email is required' }) @IsEmail({}, { message: 'Invalid email format' }) email: string; @IsNotEmpty({ message: 'Password is required' }) @MinLength(8, { message: 'Password must be at least 8 characters' }) @MaxLength(50, { message: 'Password cannot exceed 50 characters' }) @Matches( /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, { message: 'Password must contain at least one uppercase, one lowercase, and one number' } ) password: string; @IsNotEmpty({ message: 'First name is required' }) @IsString() @MinLength(2) @MaxLength(50) firstName: string; @IsNotEmpty({ message: 'Last name is required' }) @IsString() @MinLength(2) @MaxLength(50) lastName: string; @IsOptional() @IsString() @MaxLength(20) phone?: string; } ``` Das `UpdateUserDto` nutzt `PartialType` und `OmitType`, um alle Felder optional zu machen und das Passwort von Standard-Updates auszuschliessen. ```typescript // src/users/dto/update-user.dto.ts import { PartialType, OmitType } from '@nestjs/mapped-types'; import { CreateUserDto } from './create-user.dto'; // All fields from CreateUserDto become optional // Password is excluded from standard updates export class UpdateUserDto extends PartialType( OmitType(CreateUserDto, ['password'] as const) ) {} ``` Die User-Entitaet definiert die vollstaendige Datenstruktur einschliesslich automatisch generierter Felder. ```typescript // src/users/entities/user.entity.ts export class User { id: number; email: string; password: string; firstName: string; lastName: string; phone?: string; createdAt: Date; updatedAt: Date; } ``` ## Zentralisierte Fehlerbehandlung Exception Filter fangen Fehler ab und formatieren die HTTP-Antworten einheitlich. Der `HttpExceptionFilter` behandelt bekannte HTTP-Fehler, waehrend der `AllExceptionsFilter` unerwartete Systemfehler abfaengt. ```typescript // src/common/filters/http-exception.filter.ts import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, } from '@nestjs/common'; import { Request, Response } from 'express'; // Catches all HttpExceptions @Catch(HttpException) export class HttpExceptionFilter implements ExceptionFilter { catch(exception: HttpException, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); const status = exception.getStatus(); // Retrieve error message (can be string or object) const exceptionResponse = exception.getResponse(); const message = typeof exceptionResponse === 'string' ? exceptionResponse : (exceptionResponse as any).message; // Standardized response format response.status(status).json({ success: false, statusCode: status, timestamp: new Date().toISOString(), path: request.url, method: request.method, message: message, }); } } ``` ```typescript // src/common/filters/all-exceptions.filter.ts import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, } from '@nestjs/common'; import { Request, Response } from 'express'; // Catches ALL exceptions (including system errors) @Catch() export class AllExceptionsFilter implements ExceptionFilter { catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); // Determine HTTP code and message const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; const message = exception instanceof HttpException ? exception.message : 'Internal server error'; // Log error for debugging console.error('Exception caught:', exception); response.status(status).json({ success: false, statusCode: status, timestamp: new Date().toISOString(), path: request.url, message: message, }); } } ``` Beide Filter werden in der `main.ts` global registriert. ```typescript // src/main.ts (updated) import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; import { HttpExceptionFilter } from './common/filters/http-exception.filter'; import { AllExceptionsFilter } from './common/filters/all-exceptions.filter'; async function bootstrap() { const app = await NestFactory.create(AppModule); // Global validation app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, })); // Global exception filters app.useGlobalFilters( new AllExceptionsFilter(), new HttpExceptionFilter(), ); app.setGlobalPrefix('api'); await app.listen(3000); } bootstrap(); ``` > **Reihenfolge der Filter** > > Die Reihenfolge der Filter bei `useGlobalFilters()` ist entscheidend. Der zuletzt registrierte Filter wird zuerst ausgefuehrt. Der `HttpExceptionFilter` muss nach dem `AllExceptionsFilter` stehen, damit HTTP-Exceptions spezifisch behandelt werden, bevor der allgemeine Filter greift. ## Integration mit Prisma ORM Prisma bietet typsicheren Datenbankzugriff mit automatisch generierten Typen. Die Installation und Einrichtung mit PostgreSQL erfolgt in wenigen Schritten. ```bash # terminal # Install Prisma npm install prisma @prisma/client # Initialize Prisma with PostgreSQL npx prisma init --datasource-provider postgresql ``` Das Prisma-Schema definiert die Datenbankmodelle und deren Beziehungen. ```prisma // prisma/schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id Int @id @default(autoincrement()) email String @unique password String firstName String @map("first_name") lastName String @map("last_name") phone String? createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations posts Post[] @@map("users") } model Post { id Int @id @default(autoincrement()) title String content String published Boolean @default(false) authorId Int @map("author_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relation to User author User @relation(fields: [authorId], references: [id]) @@map("posts") } ``` Der `PrismaService` kapselt den Prisma-Client und verwaltet die Datenbankverbindung ueber die NestJS-Lifecycle-Hooks. ```typescript // src/prisma/prisma.service.ts import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { // Automatic connection on module startup async onModuleInit() { await this.$connect(); } // Clean disconnection on application shutdown async onModuleDestroy() { await this.$disconnect(); } } ``` Das `PrismaModule` wird mit `@Global()` dekoriert, damit der `PrismaService` in der gesamten Anwendung verfuegbar ist, ohne ihn in jedem Modul einzeln importieren zu muessen. ```typescript // src/prisma/prisma.module.ts import { Global, Module } from '@nestjs/common'; import { PrismaService } from './prisma.service'; // @Global makes the service available throughout the application @Global() @Module({ providers: [PrismaService], exports: [PrismaService], }) export class PrismaModule {} ``` Der aktualisierte `UsersService` ersetzt den In-Memory-Speicher durch Prisma-Abfragen und fuegt Passwort-Hashing sowie E-Mail-Eindeutigkeitspruefungen hinzu. ```typescript // src/users/users.service.ts (Prisma version) import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { User } from '@prisma/client'; import * as bcrypt from 'bcrypt'; @Injectable() export class UsersService { constructor(private readonly prisma: PrismaService) {} async create(createUserDto: CreateUserDto): Promise> { // Check email uniqueness const existingUser = await this.prisma.user.findUnique({ where: { email: createUserDto.email }, }); if (existingUser) { throw new ConflictException('This email is already in use'); } // Hash the password const hashedPassword = await bcrypt.hash(createUserDto.password, 10); // Create the user const user = await this.prisma.user.create({ data: { ...createUserDto, password: hashedPassword, }, }); // Exclude password from response const { password, ...result } = user; return result; } async findAll(page: number, limit: number): Promise<{ data: User[]; total: number }> { // Parallel execution of count and paginated query const [data, total] = await Promise.all([ this.prisma.user.findMany({ skip: (page - 1) * limit, take: limit, orderBy: { createdAt: 'desc' }, select: { id: true, email: true, firstName: true, lastName: true, phone: true, createdAt: true, updatedAt: true, }, }), this.prisma.user.count(), ]); return { data: data as User[], total }; } async findOne(id: number): Promise> { const user = await this.prisma.user.findUnique({ where: { id }, select: { id: true, email: true, firstName: true, lastName: true, phone: true, createdAt: true, updatedAt: true, }, }); if (!user) { throw new NotFoundException(`User with ID ${id} not found`); } return user as Omit; } async update(id: number, updateUserDto: UpdateUserDto): Promise> { // Check existence await this.findOne(id); const user = await this.prisma.user.update({ where: { id }, data: updateUserDto, select: { id: true, email: true, firstName: true, lastName: true, phone: true, createdAt: true, updatedAt: true, }, }); return user as Omit; } async remove(id: number): Promise { await this.findOne(id); await this.prisma.user.delete({ where: { id } }); } } ``` ## Interceptors fuer Response-Transformation Interceptors verarbeiten Anfragen und Antworten uebergreifend. Der `TransformInterceptor` kapselt alle Antworten in ein einheitliches Format, waehrend der `LoggingInterceptor` Ausfuehrungszeiten protokolliert. ```typescript // src/common/interceptors/transform.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler, } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; // Interface for standardized response format export interface ApiResponse { success: boolean; 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(), })), ); } } ``` ```typescript // 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(LoggingInterceptor.name); intercept(context: ExecutionContext, next: CallHandler): Observable { const request = context.switchToHttp().getRequest(); const { method, url } = request; const now = Date.now(); return next.handle().pipe( tap(() => { const response = context.switchToHttp().getResponse(); const { statusCode } = response; const duration = Date.now() - now; // Structured log format this.logger.log( `${method} ${url} ${statusCode} - ${duration}ms` ); }), ); } } ``` Der `TransformInterceptor` stellt sicher, dass alle Endpunkte dasselbe Antwortformat verwenden: `{ success, data, timestamp }`. Der `LoggingInterceptor` misst die Ausfuehrungsdauer jeder Anfrage und schreibt strukturierte Logs, die fuer Monitoring-Tools verwertbar sind. ## Fazit Diese Anleitung hat alle Kernkonzepte abgedeckt, die fuer den Aufbau einer produktionsreifen REST-API mit NestJS erforderlich sind. Von der modularen Architektur ueber Validierung bis hin zur Datenbankintegration bietet NestJS ein durchdachtes Gesamtpaket. ### Checkliste fuer die REST-API - NestJS-Projekt mit der CLI erstellen und `ValidationPipe` global konfigurieren - Code in Feature-Module mit klarer Trennung von Controller und Service aufteilen - DTOs mit `class-validator`-Decorators fuer automatische Datenvalidierung definieren - Zentralisierte Fehlerbehandlung mit Exception Filters einrichten - Prisma fuer typsicheren Datenbankzugriff integrieren - Interceptors fuer einheitliche Antwortformate und Logging implementieren Mit dieser Grundlage laesst sich die API durch Guards fuer Authentifizierung, Swagger fuer API-Dokumentation und Middleware fuer Rate Limiting erweitern. Die modulare Architektur von NestJS sorgt dafuer, dass die Codebasis auch bei wachsender Komplexitaet uebersichtlich und wartbar bleibt. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/de/blog/node-nestjs/nestjs-building-complete-rest-api