8 Controller in NestJS

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.

8.1 Was ist ein Controller?

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.

8.1.1 Grundfunktionen

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.

8.1.2 Einfaches Controller-Beispiel

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:

8.1.2.1 Controller-Registrierung

Controller 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 {}

8.1.2.2 Erweiterte Controller-Struktur

// 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);
  }
}

8.2 Routing und HTTP-Methoden

NestJS bietet ein umfangreiches Routing-System, das auf Express.js aufbaut und erweiterte Features für die API-Entwicklung bereitstellt.

8.2.1 Grundlagen des Controller-Routings

8.2.1.1 Route-Pfad-Kombinationen

@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) {}
}

8.2.1.2 HTTP-Method-Mapping

@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() {}
}

8.2.2 Deklarative Routen mit Dekoratoren

8.2.2.1 Custom Route-Dekoratoren

// 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) {}
}

8.2.2.2 Route-Constraints

@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) {}
}

8.2.3 Dynamische Parameter

8.2.3.1 URL-Parameter-Extraktion

@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,
  ) {}
}

8.2.3.2 Custom Parameter-Dekoratoren

// 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,
  ) {}
}

8.2.4 Query-Parameter und Request-Body

8.2.4.1 Query-Parameter-Handling

// 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[]) {}
}

8.2.4.2 Request-Body-Verarbeitung

// 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 },
  ) {}
}

8.3 Request-Handling im Detail

8.3.1 Request Object

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.

8.3.1.1 Vollständiger Request-Zugriff

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,
    };
  }
}

8.3.1.2 Spezifische Request-Daten

@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,
      })),
    };
  }
}

8.3.1.3 Custom Request-Processing

// 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,
    });
  }
}

8.3.2 Response Object

NestJS abstrahiert normalerweise die Response-Generierung, aber manchmal ist direkter Zugriff auf das Response-Object erforderlich.

8.3.2.1 Standard Response-Handling

@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' };
  }
}

8.3.2.2 Direkter Response-Zugriff

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',
    });
  }
}

8.3.3 Headers verwalten

8.3.3.1 Response-Headers setzen

@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() };
  }
}

8.3.3.2 CORS und Security Headers

// 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' };
  }
}

8.3.4 Redirection

@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}`);
  }
}

8.4 Erweiterte Controller-Funktionen

8.4.1 Route Wildcards

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,
    };
  }
}

8.4.2 Sub-Domain Routing

// 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' };
  }
}

8.4.3 Versioning von APIs

NestJS bietet eingebaute API-Versionierung mit verschiedenen Strategien.

8.4.3.1 URI Versioning

// 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' };
  }
}

8.4.3.2 Header Versioning

// 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 },
      ],
    };
  }
}

8.4.3.3 Media Type Versioning

// 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: [] };
  }
}

8.4.3.4 Custom Versioning

// 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());

8.5 RESTful-Ressourcen

8.5.1 CRUD-Operationen

NestJS folgt RESTful-Konventionen und macht die Implementierung von CRUD-Operationen intuitiv.

8.5.1.1 Standard CRUD Controller

// 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);
  }
}

8.5.2 Ressourcen mit dem CLI generieren

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

8.5.2.1 Anpassung der generierten Ressource

// 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);
  }
}

8.5.3 Standard REST-Konventionen

8.5.3.1 HTTP-Method und Status-Code Mapping

@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() {}
}

8.5.3.2 Nested Resources

@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);
  }
}

8.5.3.3 Pagination und Filtering

// 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.