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.

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.
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.
# 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:devDie Datei main.ts dient als Einstiegspunkt der Anwendung. Hier werden globale Pipes, Praefix und weitere Konfigurationen festgelegt.
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.
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.
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.
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<User> {
// 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<User> {
// 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<User> {
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<void> {
await this.usersService.remove(id);
}
}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.
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<User> {
// 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<User> {
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<User> {
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<void> {
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<User | undefined> {
return this.users.find(u => u.email === email);
}
}Bereit für deine Node.js / NestJS-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
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.
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.
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.
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.
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<Response>();
const request = ctx.getRequest<Request>();
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,
});
}
}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<Response>();
const request = ctx.getRequest<Request>();
// 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.
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();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.
# terminal
# Install Prisma
npm install prisma @prisma/client
# Initialize Prisma with PostgreSQL
npx prisma init --datasource-provider postgresqlDas Prisma-Schema definiert die Datenbankmodelle und deren Beziehungen.
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.
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.
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.
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<Omit<User, 'password'>> {
// 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<Omit<User, 'password'>> {
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<User, 'password'>;
}
async update(id: number, updateUserDto: UpdateUserDto): Promise<Omit<User, 'password'>> {
// 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<User, 'password'>;
}
async remove(id: number): Promise<void> {
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.
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<T> {
success: boolean;
data: T;
timestamp: string;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> {
return next.handle().pipe(
map(data => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
);
}
}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<any> {
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
ValidationPipeglobal 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
Fang an zu üben!
Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.
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.
Tags
Teilen
