Controller sind die Eingangsschicht einer NestJS-Anwendung und verantwortlich für die Verarbeitung eingehender HTTP-Requests. Sie definieren die API-Endpunkte, extrahieren Request-Daten und orchestrieren die Interaktion zwischen der HTTP-Schicht und der Geschäftslogik. Controller folgen dem Single Responsibility Principle und sollten dünn gehalten werden, während die eigentliche Geschäftslogik in Services implementiert wird.
Ein Controller in NestJS ist eine TypeScript-Klasse, die mit dem
@Controller()-Dekorator annotiert ist. Controller
gruppieren verwandte Request-Handler und definieren Routen für
verschiedene HTTP-Endpunkte. Sie fungieren als Brücke zwischen der
HTTP-Schicht und der Anwendungslogik.
Controller erfüllen mehrere wesentliche Funktionen in einer NestJS-Anwendung:
Request-Routing: Controller definieren URL-Pfade und
ordnen sie spezifischen Handler-Methoden zu. Jede Methode kann mit
HTTP-Method-Dekoratoren wie @Get(), @Post(),
@Put() oder @Delete() annotiert werden.
Parameter-Extraktion: Controller extrahieren Daten aus verschiedenen Request-Teilen wie URL-Parametern, Query-Strings, Request-Body oder Headers und machen sie für Handler-Methoden verfügbar.
Response-Generierung: Controller verarbeiten die Rückgabewerte von Services und transformieren sie in HTTP-Responses mit entsprechenden Status-Codes und Headers.
Middleware-Integration: Controller können Middleware, Guards, Interceptors und Pipes auf verschiedenen Ebenen anwenden, um Cross-Cutting Concerns wie Authentifizierung, Validierung und Logging zu handhaben.
Exception-Handling: Controller fangen Exceptions ab und transformieren sie in appropriate HTTP-Error-Responses.
Beginnen wir mit einem grundlegenden Controller-Beispiel:
// src/cats/cats.controller.ts
import { Controller, Get, Post, Body, Param, Delete, Put } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { UpdateCatDto } from './dto/update-cat.dto';
import { Cat } from './interfaces/cat.interface';
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Post()
async create(@Body() createCatDto: CreateCatDto): Promise<Cat> {
return this.catsService.create(createCatDto);
}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
@Get(':id')
async findOne(@Param('id') id: string): Promise<Cat> {
return this.catsService.findOne(id);
}
@Put(':id')
async update(
@Param('id') id: string,
@Body() updateCatDto: UpdateCatDto,
): Promise<Cat> {
return this.catsService.update(id, updateCatDto);
}
@Delete(':id')
async remove(@Param('id') id: string): Promise<void> {
return this.catsService.remove(id);
}
}Erklärung der Komponenten:
@Controller('cats'): Definiert den Base-Pfad für alle
Routen in diesem Controller@Get(), @Post(), etc.:
HTTP-Method-Dekoratoren, die Handler-Methoden mit HTTP-Verben
verknüpfen@Body(): Extrahiert den Request-Body und wendet
automatische Validation an@Param('id'): Extrahiert URL-ParameterCatsService wird über den
Constructor injiziertController müssen in einem Modul registriert werden:
// src/cats/cats.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}// src/users/users.controller.ts
import {
Controller,
Get,
Post,
Body,
Param,
Query,
Delete,
Put,
Patch,
HttpCode,
HttpStatus,
UseGuards,
UseInterceptors,
UsePipes,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { UserQueryDto } from './dto/user-query.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { LoggingInterceptor } from '../common/interceptors/logging.interceptor';
import { ValidationPipe } from '../common/pipes/validation.pipe';
import { User } from './entities/user.entity';
@ApiTags('users')
@Controller('users')
@UseGuards(JwtAuthGuard)
@UseInterceptors(LoggingInterceptor)
@ApiBearerAuth()
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
@ApiOperation({ summary: 'Create a new user' })
@ApiResponse({ status: 201, description: 'User successfully created', type: User })
@ApiResponse({ status: 400, description: 'Bad Request' })
@HttpCode(HttpStatus.CREATED)
@UsePipes(new ValidationPipe({ transform: true }))
async create(@Body() createUserDto: CreateUserDto): Promise<User> {
return this.usersService.create(createUserDto);
}
@Get()
@ApiOperation({ summary: 'Get all users' })
@ApiResponse({ status: 200, description: 'Users retrieved successfully', type: [User] })
async findAll(@Query() query: UserQueryDto): Promise<User[]> {
return this.usersService.findAll(query);
}
@Get('profile')
@ApiOperation({ summary: 'Get current user profile' })
async getProfile(@CurrentUser() user: User): Promise<User> {
return user;
}
@Get(':id')
@ApiOperation({ summary: 'Get user by ID' })
@ApiResponse({ status: 200, description: 'User found', type: User })
@ApiResponse({ status: 404, description: 'User not found' })
async findOne(@Param('id') id: string): Promise<User> {
return this.usersService.findOne(id);
}
@Put(':id')
@ApiOperation({ summary: 'Update user' })
@UseGuards(RolesGuard)
@Roles('admin', 'user')
async update(
@Param('id') id: string,
@Body() updateUserDto: UpdateUserDto,
@CurrentUser() currentUser: User,
): Promise<User> {
return this.usersService.update(id, updateUserDto, currentUser);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete user' })
@UseGuards(RolesGuard)
@Roles('admin')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id') id: string): Promise<void> {
return this.usersService.remove(id);
}
}NestJS bietet ein umfangreiches Routing-System, das auf Express.js aufbaut und erweiterte Features für die API-Entwicklung bereitstellt.
@Controller('api/v1/users')
export class UsersController {
@Get() // GET /api/v1/users
findAll() {}
@Get('active') // GET /api/v1/users/active
findActive() {}
@Get(':id') // GET /api/v1/users/123
findOne(@Param('id') id: string) {}
@Get(':id/posts') // GET /api/v1/users/123/posts
getUserPosts(@Param('id') id: string) {}
}@Controller('products')
export class ProductsController {
@Get() // GET /products
getAllProducts() {}
@Post() // POST /products
createProduct(@Body() createProductDto: CreateProductDto) {}
@Put(':id') // PUT /products/123
updateProduct(@Param('id') id: string, @Body() updateProductDto: UpdateProductDto) {}
@Patch(':id') // PATCH /products/123
partialUpdate(@Param('id') id: string, @Body() partialUpdateDto: PartialUpdateDto) {}
@Delete(':id') // DELETE /products/123
deleteProduct(@Param('id') id: string) {}
@Head(':id') // HEAD /products/123
checkProductExists(@Param('id') id: string) {}
@Options() // OPTIONS /products
getProductOptions() {}
}// src/common/decorators/api-route.decorator.ts
import { applyDecorators, UseGuards, UseInterceptors } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { LoggingInterceptor } from '../interceptors/logging.interceptor';
export function ApiRoute(options: {
summary: string;
description?: string;
requireAuth?: boolean;
responses?: { status: number; description: string; type?: any }[];
}) {
const decorators = [
ApiOperation({ summary: options.summary, description: options.description }),
];
if (options.requireAuth) {
decorators.push(UseGuards(JwtAuthGuard), ApiBearerAuth());
}
decorators.push(UseInterceptors(LoggingInterceptor));
if (options.responses) {
options.responses.forEach(response => {
decorators.push(ApiResponse(response));
});
}
return applyDecorators(...decorators);
}
// Verwendung
@Controller('products')
export class ProductsController {
@Get(':id')
@ApiRoute({
summary: 'Get product by ID',
description: 'Retrieves a product by its unique identifier',
requireAuth: true,
responses: [
{ status: 200, description: 'Product found', type: Product },
{ status: 404, description: 'Product not found' },
],
})
findOne(@Param('id') id: string) {}
}@Controller('files')
export class FilesController {
// Nur numerische IDs akzeptieren
@Get(':id(\\d+)')
findById(@Param('id') id: number) {}
// UUID-Format erzwingen
@Get(':uuid([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})')
findByUuid(@Param('uuid') uuid: string) {}
// File-Extensions validieren
@Get(':filename(.+\\.(jpg|jpeg|png|gif))')
getImage(@Param('filename') filename: string) {}
}@Controller('users')
export class UsersController {
// Einzelner Parameter
@Get(':id')
findOne(@Param('id') id: string) {}
// Multiple Parameter
@Get(':userId/posts/:postId')
getUserPost(
@Param('userId') userId: string,
@Param('postId') postId: string,
) {}
// Alle Parameter als Objekt
@Get(':userId/posts/:postId/comments/:commentId')
getComment(@Param() params: { userId: string; postId: string; commentId: string }) {}
// Parameter mit Validation Pipes
@Get(':id')
findOneWithValidation(
@Param('id', ParseUUIDPipe) id: string,
) {}
@Get(':userId/posts/:postId')
getUserPostValidated(
@Param('userId', ParseUUIDPipe) userId: string,
@Param('postId', ParseIntPipe) postId: number,
) {}
}// src/common/decorators/user-id.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const UserId = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user?.id;
},
);
// src/common/decorators/validated-param.decorator.ts
import { createParamDecorator, ExecutionContext, BadRequestException } from '@nestjs/common';
export const ValidatedParam = createParamDecorator(
(paramName: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const value = request.params[paramName];
if (!value) {
throw new BadRequestException(`Parameter ${paramName} is required`);
}
return value;
},
);
// Verwendung
@Controller('users')
export class UsersController {
@Get(':id/profile')
getProfile(
@ValidatedParam('id') userId: string,
@UserId() currentUserId: string,
) {}
}// src/products/dto/product-query.dto.ts
import { IsOptional, IsString, IsNumber, IsEnum, Min, Max } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiPropertyOptional } from '@nestjs/swagger';
export enum SortOrder {
ASC = 'asc',
DESC = 'desc',
}
export class ProductQueryDto {
@ApiPropertyOptional({ description: 'Search term' })
@IsOptional()
@IsString()
search?: string;
@ApiPropertyOptional({ description: 'Category filter' })
@IsOptional()
@IsString()
category?: string;
@ApiPropertyOptional({ description: 'Minimum price' })
@IsOptional()
@IsNumber()
@Min(0)
@Type(() => Number)
minPrice?: number;
@ApiPropertyOptional({ description: 'Maximum price' })
@IsOptional()
@IsNumber()
@Max(10000)
@Type(() => Number)
maxPrice?: number;
@ApiPropertyOptional({ description: 'Page number', minimum: 1, default: 1 })
@IsOptional()
@IsNumber()
@Min(1)
@Type(() => Number)
page?: number = 1;
@ApiPropertyOptional({ description: 'Items per page', minimum: 1, maximum: 100, default: 10 })
@IsOptional()
@IsNumber()
@Min(1)
@Max(100)
@Type(() => Number)
limit?: number = 10;
@ApiPropertyOptional({ description: 'Sort field' })
@IsOptional()
@IsString()
sortBy?: string = 'createdAt';
@ApiPropertyOptional({ description: 'Sort order', enum: SortOrder })
@IsOptional()
@IsEnum(SortOrder)
sortOrder?: SortOrder = SortOrder.DESC;
}
@Controller('products')
export class ProductsController {
@Get()
async findAll(@Query() query: ProductQueryDto) {
return this.productsService.findAll(query);
}
// Einzelne Query-Parameter
@Get('search')
search(
@Query('q') searchTerm: string,
@Query('category') category?: string,
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page?: number,
) {}
// Array von Query-Parametern
@Get('by-tags')
findByTags(@Query('tags') tags: string[]) {}
}// src/users/dto/create-user.dto.ts
import {
IsEmail,
IsString,
IsOptional,
IsDateString,
IsPhoneNumber,
IsEnum,
MinLength,
MaxLength,
Matches,
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export enum UserRole {
USER = 'user',
ADMIN = 'admin',
MODERATOR = 'moderator',
}
export class CreateUserDto {
@ApiProperty({ description: 'User email address' })
@IsEmail()
email: string;
@ApiProperty({ description: 'User password', minLength: 8 })
@IsString()
@MinLength(8)
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/, {
message: 'Password must contain uppercase, lowercase, number and special character',
})
password: string;
@ApiProperty({ description: 'User first name' })
@IsString()
@MinLength(1)
@MaxLength(50)
firstName: string;
@ApiProperty({ description: 'User last name' })
@IsString()
@MinLength(1)
@MaxLength(50)
lastName: string;
@ApiPropertyOptional({ description: 'User phone number' })
@IsOptional()
@IsPhoneNumber('DE')
phoneNumber?: string;
@ApiPropertyOptional({ description: 'User birth date' })
@IsOptional()
@IsDateString()
birthDate?: string;
@ApiPropertyOptional({ description: 'User role', enum: UserRole, default: UserRole.USER })
@IsOptional()
@IsEnum(UserRole)
role?: UserRole = UserRole.USER;
}
@Controller('users')
export class UsersController {
@Post()
async create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
// Partial Body Updates
@Patch(':id')
async partialUpdate(
@Param('id') id: string,
@Body() updateUserDto: Partial<CreateUserDto>,
) {}
// File Upload mit Body
@Post('upload')
@UseInterceptors(FileInterceptor('avatar'))
async uploadAvatar(
@UploadedFile() file: Express.Multer.File,
@Body() metadata: { description?: string },
) {}
}Das Request-Object enthält alle Informationen über die eingehende HTTP-Anfrage. NestJS stellt verschiedene Dekoratoren zur Verfügung, um spezifische Teile des Requests zu extrahieren.
import { Controller, Get, Req, Res } from '@nestjs/common';
import { Request, Response } from 'express';
@Controller('debug')
export class DebugController {
@Get('request-info')
getRequestInfo(@Req() request: Request) {
return {
method: request.method,
url: request.url,
headers: request.headers,
query: request.query,
params: request.params,
body: request.body,
ip: request.ip,
userAgent: request.get('User-Agent'),
protocol: request.protocol,
secure: request.secure,
hostname: request.hostname,
};
}
}@Controller('api')
export class ApiController {
@Get('headers')
getHeaders(
@Headers() headers: Record<string, string>,
@Headers('authorization') authHeader: string,
@Headers('user-agent') userAgent: string,
) {
return {
allHeaders: headers,
authorization: authHeader,
userAgent,
};
}
@Get('client-info')
getClientInfo(
@Ip() clientIp: string,
@Headers('user-agent') userAgent: string,
@Headers('x-forwarded-for') forwardedFor?: string,
) {
return {
ip: clientIp,
userAgent,
realIp: forwardedFor || clientIp,
};
}
@Post('form-data')
@UseInterceptors(AnyFilesInterceptor())
handleFormData(
@Body() body: any,
@UploadedFiles() files: Array<Express.Multer.File>,
) {
return {
formFields: body,
uploadedFiles: files.map(file => ({
fieldname: file.fieldname,
originalname: file.originalname,
mimetype: file.mimetype,
size: file.size,
})),
};
}
}// src/common/decorators/request-info.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export interface RequestInfo {
ip: string;
userAgent: string;
timestamp: Date;
correlationId: string;
}
export const RequestInfo = createParamDecorator(
(data: unknown, ctx: ExecutionContext): RequestInfo => {
const request = ctx.switchToHttp().getRequest();
return {
ip: request.ip || request.connection.remoteAddress,
userAgent: request.get('User-Agent') || 'Unknown',
timestamp: new Date(),
correlationId: request.headers['x-correlation-id'] ||
request.headers['x-request-id'] ||
crypto.randomUUID(),
};
},
);
// Verwendung
@Controller('analytics')
export class AnalyticsController {
@Post('events')
trackEvent(
@Body() eventData: EventDto,
@RequestInfo() requestInfo: RequestInfo,
) {
return this.analyticsService.trackEvent({
...eventData,
metadata: requestInfo,
});
}
}NestJS abstrahiert normalerweise die Response-Generierung, aber manchmal ist direkter Zugriff auf das Response-Object erforderlich.
@Controller('responses')
export class ResponsesController {
// Automatische Serialisierung
@Get('auto')
getAutoResponse() {
return { message: 'Success', timestamp: new Date() };
}
// Custom Status Codes
@Post('created')
@HttpCode(HttpStatus.CREATED)
createResource() {
return { id: 123, created: true };
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
deleteResource(@Param('id') id: string) {
// Void return für 204 No Content
}
// Custom Headers
@Get('with-headers')
@Header('Cache-Control', 'max-age=3600')
@Header('X-Custom-Header', 'Custom-Value')
getWithHeaders() {
return { data: 'cached for 1 hour' };
}
}import { Controller, Get, Res, StreamableFile } from '@nestjs/common';
import { Response } from 'express';
import { createReadStream } from 'fs';
import { join } from 'path';
@Controller('files')
export class FilesController {
@Get('download/:filename')
downloadFile(
@Param('filename') filename: string,
@Res({ passthrough: true }) response: Response,
) {
const file = createReadStream(join(process.cwd(), 'uploads', filename));
response.set({
'Content-Type': 'application/octet-stream',
'Content-Disposition': `attachment; filename="${filename}"`,
});
return new StreamableFile(file);
}
@Get('custom-response')
customResponse(@Res() response: Response) {
response
.status(200)
.set('Content-Type', 'text/plain')
.send('Custom response content');
}
@Get('json-with-custom-status')
jsonWithCustomStatus(@Res() response: Response) {
response.status(202).json({
message: 'Accepted for processing',
status: 'pending',
});
}
}@Controller('headers')
export class HeadersController {
// Statische Headers
@Get('static')
@Header('X-API-Version', '1.0')
@Header('X-Rate-Limit', '1000')
getWithStaticHeaders() {
return { message: 'Headers set statically' };
}
// Dynamische Headers
@Get('dynamic')
getDynamicHeaders(@Res({ passthrough: true }) response: Response) {
const timestamp = new Date().toISOString();
const requestId = crypto.randomUUID();
response.set({
'X-Timestamp': timestamp,
'X-Request-ID': requestId,
'X-Server': 'NestJS-API',
});
return {
message: 'Headers set dynamically',
timestamp,
requestId,
};
}
// Conditional Headers
@Get('conditional/:type')
getConditionalHeaders(
@Param('type') type: string,
@Res({ passthrough: true }) response: Response,
) {
if (type === 'json') {
response.set('Content-Type', 'application/json');
} else if (type === 'xml') {
response.set('Content-Type', 'application/xml');
}
return { type, timestamp: new Date() };
}
}// src/common/interceptors/security-headers.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class SecurityHeadersInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const response = context.switchToHttp().getResponse();
response.set({
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'Referrer-Policy': 'strict-origin-when-cross-origin',
});
return next.handle();
}
}
@Controller('secure')
@UseInterceptors(SecurityHeadersInterceptor)
export class SecureController {
@Get('data')
getSecureData() {
return { message: 'Secure data with security headers' };
}
}@Controller('redirect')
export class RedirectController {
// Einfache Umleitung
@Get('simple')
@Redirect('https://example.com', 302)
simpleRedirect() {}
// Dynamische Umleitung
@Get('dynamic/:target')
dynamicRedirect(@Param('target') target: string) {
const redirectUrls = {
docs: 'https://docs.nestjs.com',
github: 'https://github.com/nestjs/nest',
npm: 'https://www.npmjs.com/package/@nestjs/core',
};
const url = redirectUrls[target];
if (!url) {
throw new NotFoundException('Redirect target not found');
}
return { url, statusCode: 301 };
}
// Conditional Redirect
@Get('conditional')
conditionalRedirect(
@Query('mobile') isMobile: string,
@Headers('user-agent') userAgent: string,
) {
const isMobileDevice = isMobile === 'true' ||
/Mobile|Android|iPhone/i.test(userAgent);
if (isMobileDevice) {
return { url: '/mobile/app', statusCode: 302 };
}
return { message: 'Welcome to desktop version' };
}
// Temporäre Umleitung mit Response-Object
@Get('temporary/:id')
temporaryRedirect(
@Param('id') id: string,
@Res() response: Response,
) {
response.redirect(302, `/new-location/${id}`);
}
}NestJS unterstützt Pattern-basiertes Routing mit Wildcards für flexible URL-Strukturen.
@Controller('wildcards')
export class WildcardController {
// Asterisk wildcard - matches any sequence
@Get('files/*')
getFile(@Req() request: Request) {
const filePath = request.params[0]; // Everything after 'files/'
return { filePath };
}
// Question mark - optional character
@Get('colou?r')
getColor() {
return { message: 'Matches both "color" and "colour"' };
}
// Plus - one or more
@Get('ab+cd')
getAbcd() {
return { message: 'Matches "abcd", "abbcd", "abbbcd", etc.' };
}
// Parentheses - grouping
@Get('ab(cd)?e')
getOptionalGroup() {
return { message: 'Matches "abe" and "abcde"' };
}
// Complex patterns
@Get('users/:userId/posts/:postId?/comments/*')
handleNestedResource(
@Param('userId') userId: string,
@Param('postId') postId: string,
@Req() request: Request,
) {
const commentPath = request.params[0];
return {
userId,
postId: postId || null,
commentPath,
};
}
}// src/common/decorators/subdomain.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const SUBDOMAIN_KEY = 'subdomain';
export const Subdomain = (subdomain: string) => SetMetadata(SUBDOMAIN_KEY, subdomain);
// src/common/guards/subdomain.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { SUBDOMAIN_KEY } from '../decorators/subdomain.decorator';
@Injectable()
export class SubdomainGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredSubdomain = this.reflector.getAllAndOverride<string>(
SUBDOMAIN_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredSubdomain) {
return true;
}
const request = context.switchToHttp().getRequest();
const host = request.get('host');
const subdomain = host.split('.')[0];
return subdomain === requiredSubdomain;
}
}
@Controller('api')
@UseGuards(SubdomainGuard)
export class ApiController {
@Get('users')
@Subdomain('admin')
getAdminUsers() {
return { message: 'Admin users - only accessible via admin.example.com' };
}
@Get('stats')
@Subdomain('analytics')
getAnalytics() {
return { message: 'Analytics - only accessible via analytics.example.com' };
}
@Get('public')
getPublicData() {
return { message: 'Public data - accessible from any subdomain' };
}
}NestJS bietet eingebaute API-Versionierung mit verschiedenen Strategien.
// main.ts
import { VersioningType } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableVersioning({
type: VersioningType.URI,
prefix: 'api/v',
});
await app.listen(3000);
}
// Controller mit Versionierung
@Controller('users')
export class UsersController {
@Get()
@Version('1')
findAllV1() {
return { version: '1.0', users: ['user1', 'user2'] };
}
@Get()
@Version('2')
findAllV2() {
return {
version: '2.0',
users: [
{ id: 1, name: 'user1', email: 'user1@example.com' },
{ id: 2, name: 'user2', email: 'user2@example.com' },
],
};
}
@Get(':id')
@Version(['1', '2'])
findOne(@Param('id') id: string) {
return { id, message: 'Available in both v1 and v2' };
}
@Post()
@Version('2')
createUser(@Body() createUserDto: CreateUserDto) {
return { message: 'Only available in v2' };
}
}// main.ts
app.enableVersioning({
type: VersioningType.HEADER,
header: 'X-API-Version',
});
@Controller('products')
export class ProductsController {
@Get()
@Version('1')
findAllV1(@Headers('X-API-Version') version: string) {
return { version, products: ['product1'] };
}
@Get()
@Version('2')
findAllV2(@Headers('X-API-Version') version: string) {
return {
version,
products: [
{ id: 1, name: 'product1', price: 100 },
],
};
}
}// main.ts
app.enableVersioning({
type: VersioningType.MEDIA_TYPE,
key: 'v=',
});
@Controller('orders')
export class OrdersController {
@Get()
@Version('1')
findAllV1() {
// Accessed with Accept: application/json;v=1
return { version: '1.0', orders: [] };
}
@Get()
@Version('2')
findAllV2() {
// Accessed with Accept: application/json;v=2
return { version: '2.0', orders: [] };
}
}// src/common/versioning/custom-versioning.ts
import { VersioningOptions, VersioningType } from '@nestjs/common';
export class CustomVersioningOptions implements VersioningOptions {
type: VersioningType.CUSTOM;
extractor(request: any): string | string[] {
// Extract version from query parameter
if (request.query.version) {
return request.query.version;
}
// Extract from custom header
if (request.headers['x-api-version']) {
return request.headers['x-api-version'];
}
// Default to v1
return '1';
}
}
// main.ts
app.enableVersioning(new CustomVersioningOptions());NestJS folgt RESTful-Konventionen und macht die Implementierung von CRUD-Operationen intuitiv.
// src/posts/posts.controller.ts
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Query,
UseGuards,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { PostsService } from './posts.service';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { PostQueryDto } from './dto/post-query.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { User } from '../users/entities/user.entity';
@ApiTags('posts')
@Controller('posts')
@UseGuards(JwtAuthGuard)
export class PostsController {
constructor(private readonly postsService: PostsService) {}
@Post()
@ApiOperation({ summary: 'Create a new post' })
@ApiResponse({ status: 201, description: 'Post created successfully' })
@HttpCode(HttpStatus.CREATED)
create(
@Body() createPostDto: CreatePostDto,
@CurrentUser() user: User,
) {
return this.postsService.create(createPostDto, user);
}
@Get()
@ApiOperation({ summary: 'Get all posts' })
@ApiResponse({ status: 200, description: 'Posts retrieved successfully' })
findAll(@Query() query: PostQueryDto) {
return this.postsService.findAll(query);
}
@Get(':id')
@ApiOperation({ summary: 'Get post by ID' })
@ApiResponse({ status: 200, description: 'Post found' })
@ApiResponse({ status: 404, description: 'Post not found' })
findOne(@Param('id') id: string) {
return this.postsService.findOne(id);
}
@Patch(':id')
@ApiOperation({ summary: 'Update post' })
@ApiResponse({ status: 200, description: 'Post updated successfully' })
update(
@Param('id') id: string,
@Body() updatePostDto: UpdatePostDto,
@CurrentUser() user: User,
) {
return this.postsService.update(id, updatePostDto, user);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete post' })
@ApiResponse({ status: 204, description: 'Post deleted successfully' })
@HttpCode(HttpStatus.NO_CONTENT)
remove(
@Param('id') id: string,
@CurrentUser() user: User,
) {
return this.postsService.remove(id, user);
}
// Zusätzliche RESTful Endpunkte
@Get(':id/comments')
@ApiOperation({ summary: 'Get post comments' })
getComments(@Param('id') id: string) {
return this.postsService.getComments(id);
}
@Post(':id/comments')
@ApiOperation({ summary: 'Add comment to post' })
addComment(
@Param('id') id: string,
@Body() commentDto: CreateCommentDto,
@CurrentUser() user: User,
) {
return this.postsService.addComment(id, commentDto, user);
}
@Patch(':id/publish')
@ApiOperation({ summary: 'Publish post' })
publish(
@Param('id') id: string,
@CurrentUser() user: User,
) {
return this.postsService.publish(id, user);
}
@Patch(':id/unpublish')
@ApiOperation({ summary: 'Unpublish post' })
unpublish(
@Param('id') id: string,
@CurrentUser() user: User,
) {
return this.postsService.unpublish(id, user);
}
}Die NestJS CLI bietet einen resource-Befehl, der
vollständige CRUD-Controller generiert.
# Vollständige REST-Ressource generieren
nest generate resource posts
# Interaktive Auswahl:
# ? What transport layer do you use? REST API
# ? Would you like to generate CRUD entry points? Yes
# Generiert:
# src/posts/
# ├── dto/
# │ ├── create-post.dto.ts
# │ └── update-post.dto.ts
# ├── entities/
# │ └── post.entity.ts
# ├── posts.controller.ts
# ├── posts.controller.spec.ts
# ├── posts.module.ts
# ├── posts.service.ts
# └── posts.service.spec.ts// Generierter Controller (angepasst)
@Controller('posts')
export class PostsController {
constructor(private readonly postsService: PostsService) {}
@Post()
create(@Body() createPostDto: CreatePostDto) {
return this.postsService.create(createPostDto);
}
@Get()
findAll() {
return this.postsService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.postsService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updatePostDto: UpdatePostDto) {
return this.postsService.update(+id, updatePostDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.postsService.remove(+id);
}
}@Controller('articles')
export class ArticlesController {
// GET /articles - List all articles
@Get()
@HttpCode(HttpStatus.OK) // 200
findAll(@Query() query: ArticleQueryDto) {}
// GET /articles/:id - Get specific article
@Get(':id')
@HttpCode(HttpStatus.OK) // 200
findOne(@Param('id') id: string) {}
// POST /articles - Create new article
@Post()
@HttpCode(HttpStatus.CREATED) // 201
create(@Body() createArticleDto: CreateArticleDto) {}
// PUT /articles/:id - Replace entire article
@Put(':id')
@HttpCode(HttpStatus.OK) // 200
replace(
@Param('id') id: string,
@Body() replaceArticleDto: ReplaceArticleDto,
) {}
// PATCH /articles/:id - Partial update
@Patch(':id')
@HttpCode(HttpStatus.OK) // 200
update(
@Param('id') id: string,
@Body() updateArticleDto: UpdateArticleDto,
) {}
// DELETE /articles/:id - Delete article
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) // 204
remove(@Param('id') id: string) {}
// HEAD /articles/:id - Check if article exists
@Head(':id')
@HttpCode(HttpStatus.OK) // 200 if exists, 404 if not
checkExists(@Param('id') id: string) {}
// OPTIONS /articles - Get allowed methods
@Options()
@HttpCode(HttpStatus.OK) // 200
@Header('Allow', 'GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS')
getOptions() {}
}@Controller('users/:userId/posts')
export class UserPostsController {
constructor(private readonly userPostsService: UserPostsService) {}
// GET /users/:userId/posts
@Get()
findUserPosts(@Param('userId') userId: string) {
return this.userPostsService.findByUserId(userId);
}
// POST /users/:userId/posts
@Post()
createUserPost(
@Param('userId') userId: string,
@Body() createPostDto: CreatePostDto,
) {
return this.userPostsService.create(userId, createPostDto);
}
// GET /users/:userId/posts/:postId
@Get(':postId')
findUserPost(
@Param('userId') userId: string,
@Param('postId') postId: string,
) {
return this.userPostsService.findOne(userId, postId);
}
}// src/common/dto/pagination.dto.ts
export class PaginationDto {
@ApiPropertyOptional({ minimum: 1, default: 1 })
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number = 1;
@ApiPropertyOptional({ minimum: 1, maximum: 100, default: 10 })
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
limit?: number = 10;
}
@Controller('products')
export class ProductsController {
@Get()
async findAll(@Query() query: ProductQueryDto & PaginationDto) {
const { data, total, page, limit } = await this.productsService.findAll(query);
return {
data,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
hasNextPage: page * limit < total,
hasPrevPage: page > 1,
},
links: {
self: `/products?page=${page}&limit=${limit}`,
next: page * limit < total ? `/products?page=${page + 1}&limit=${limit}` : null,
prev: page > 1 ? `/products?page=${page - 1}&limit=${limit}` : null,
},
};
}
}Controller in NestJS bieten eine mächtige und flexible Abstraktion für HTTP-Request-Handling. Durch die Kombination von Dekoratoren, Dependency Injection und TypeScript-Features ermöglichen sie die Entwicklung robuster, gut strukturierter APIs, die moderne Best Practices befolgen und gleichzeitig wartbar und testbar bleiben.