Skip to main content

Chapter 3: Controllers & Routing

Controllers are the entry point for handling client requests in NestJS. Think of them as the “reception desk” of your application—they receive requests, ask the right service for help, and send back a response. This chapter explores routing, request/response handling, decorators, middleware, guards, interceptors, error handling, and best practices for building robust APIs.

3.1 What is a Controller?

A controller is responsible for receiving incoming requests, delegating work to services, and returning responses. Controllers define the routes and HTTP methods for your API.

Controller Responsibilities

Controllers handle HTTP-specific concerns:
  • Route definition: Map URLs to handler methods
  • Request extraction: Get data from params, query, body, headers
  • Response formatting: Return data in the correct format
  • Error handling: Throw appropriate HTTP exceptions
  • Validation: Ensure incoming data is valid (via DTOs and pipes)
What Controllers Should NOT Do:
  • Business logic (delegate to services)
  • Database access (delegate to repositories/services)
  • Complex data transformation (delegate to services)
  • Authentication logic (use guards)
Analogy:
Imagine a restaurant: the controller is the waiter (takes orders, serves food), the service is the chef (prepares the meal), and the client is the customer.

Basic Controller Structure

import { Controller, Get } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('users')  // Base route: /users
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()  // GET /users
  findAll() {
    return this.usersService.findAll();
  }
}
Key Components:
  • @Controller('users') - Defines the base route for all methods
  • Constructor injection - Services are injected via constructor
  • Route decorators - @Get(), @Post(), etc. define HTTP methods
  • Handler methods - Process requests and return responses
Diagram: Request Flow
HTTP Request → Controller → Service → Repository/Database → Response

3.2 Routing in NestJS

Routes are defined using decorators like @Get, @Post, @Put, @Delete, @Patch, etc. The path can include parameters, query strings, and wildcards.

HTTP Method Decorators

NestJS provides decorators for all HTTP methods:
@Controller('users')
export class UsersController {
  @Get()           // GET /users
  findAll() { }

  @Get(':id')       // GET /users/:id
  findOne() { }

  @Post()           // POST /users
  create() { }

  @Put(':id')       // PUT /users/:id
  update() { }

  @Patch(':id')     // PATCH /users/:id
  partialUpdate() { }

  @Delete(':id')     // DELETE /users/:id
  remove() { }
}

Route Parameters

Extract dynamic segments from the URL:
@Get(':id')
findOne(@Param('id') id: string) {
  return this.usersService.findOne(Number(id));
}
Multiple Parameters:
@Get(':userId/posts/:postId')
findUserPost(
  @Param('userId') userId: string,
  @Param('postId') postId: string,
) {
  return this.postsService.findOne(userId, postId);
}
All Parameters:
@Get(':id')
findOne(@Param() params: { id: string }) {
  return this.usersService.findOne(Number(params.id));
}
With Pipes for Validation:
import { ParseIntPipe } from '@nestjs/common';

@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
  // id is already a number, no need to convert
  return this.usersService.findOne(id);
}

Query Parameters

Extract data from the query string:
@Get()
findByQuery(@Query('role') role: string) {
  return this.usersService.findByRole(role);
}
Multiple Query Parameters:
@Get()
findAll(
  @Query('page') page: number,
  @Query('limit') limit: number,
  @Query('sort') sort: string,
) {
  return this.usersService.findAll({ page, limit, sort });
}
All Query Parameters:
@Get()
findAll(@Query() query: { page?: number; limit?: number; sort?: string }) {
  return this.usersService.findAll(query);
}
With DTO for Validation:
export class QueryDto {
  @IsOptional()
  @IsInt()
  @Min(1)
  page?: number;

  @IsOptional()
  @IsInt()
  @Min(1)
  @Max(100)
  limit?: number;
}

@Get()
findAll(@Query() query: QueryDto) {
  return this.usersService.findAll(query);
}

Request Body

Extract data from the request body:
@Post()
create(@Body() createDto: CreateUserDto) {
  return this.usersService.create(createDto);
}
Partial Body:
@Post()
create(@Body('name') name: string) {
  // Only extract 'name' from body
  return this.usersService.create({ name });
}
Multiple Body Properties:
@Post()
create(
  @Body('name') name: string,
  @Body('email') email: string,
) {
  return this.usersService.create({ name, email });
}

Headers

Extract data from request headers:
@Get()
findAll(@Headers('authorization') auth: string) {
  return this.usersService.findAll();
}
All Headers:
@Get()
findAll(@Headers() headers: Record<string, string>) {
  const auth = headers['authorization'];
  return this.usersService.findAll();
}

Request Object

Access the full request object when needed:
import { Request } from 'express';

@Get()
findAll(@Req() req: Request) {
  // Access full request object
  return this.usersService.findAll();
}

Response Object

Control the response directly:
import { Response } from 'express';

@Get()
findAll(@Res() res: Response) {
  return res.status(200).json({ data: [] });
}
Warning: Using @Res() bypasses NestJS response handling. Use with caution, or return data and let NestJS handle the response.

Status Codes

Set custom HTTP status codes:
import { HttpCode } from '@nestjs/common';

@Post()
@HttpCode(201)  // Returns 201 Created instead of 200
create(@Body() createDto: CreateUserDto) {
  return this.usersService.create(createDto);
}
Common Status Codes:
@Post()
@HttpCode(HttpStatus.CREATED)  // 201
create() { }

@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)  // 204
remove() { }

Route Wildcards

Use wildcards for flexible routing:
@Get('ab*cd')  // Matches abcd, ab_cd, ab123cd, etc.
findWildcard() {
  return 'This route uses a wildcard';
}

3.3 Request Lifecycle

Understanding the request lifecycle helps you know where to place your logic and how different components interact.

Complete Request Lifecycle

  1. Incoming Request - HTTP request arrives at the server
  2. Middleware - Global and route-specific middleware runs
  3. Guards - Authentication and authorization checks
  4. Interceptors (Before) - Pre-processing (logging, transformation)
  5. Pipes - Validation and transformation of input data
  6. Controller - Route handler method executes
  7. Service - Business logic executes
  8. Interceptors (After) - Post-processing (response transformation)
  9. Exception Filters - Handle any exceptions
  10. Response - HTTP response sent to client
Diagram: Full Request Lifecycle
HTTP Request

Global Middleware

Route Middleware

Guards (Authentication/Authorization)

Interceptors (Before)

Pipes (Validation/Transformation)

Controller Handler

Service (Business Logic)

Interceptors (After - Response Transformation)

Exception Filters (if error)

HTTP Response

Execution Order Example

// 1. Middleware
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: Function) {
    console.log('1. Middleware');
    next();
  }
}

// 2. Guard
@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    console.log('2. Guard');
    return true;
  }
}

// 3. Interceptor (Before)
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    console.log('3. Interceptor (Before)');
    return next.handle();
  }
}

// 4. Pipe
export class ValidationPipe implements PipeTransform {
  transform(value: any) {
    console.log('4. Pipe');
    return value;
  }
}

// 5. Controller
@Controller('users')
@UseGuards(AuthGuard)
@UseInterceptors(LoggingInterceptor)
export class UsersController {
  @Get()
  @UsePipes(ValidationPipe)
  findAll() {
    console.log('5. Controller');
    return [];
  }
}

3.4 Validation & DTOs

Data Transfer Objects (DTOs) define the shape of data for requests and responses. Combined with validation, they ensure your API only accepts well-formed requests.

Why Use DTOs?

  • Type Safety: TypeScript knows the shape of your data
  • Validation: Ensure data meets requirements
  • Documentation: DTOs document your API contract
  • Transformation: Can transform data automatically
  • Security: Prevent invalid or malicious data

Basic DTO

// dto/create-user.dto.ts
export class CreateUserDto {
  name: string;
  email: string;
  age: number;
}

DTO with Validation

Install validation packages:
npm install class-validator class-transformer
import { IsString, IsEmail, IsInt, Min, Max, IsOptional } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @MinLength(2)
  @MaxLength(50)
  name: string;

  @IsEmail()
  email: string;

  @IsInt()
  @Min(18)
  @Max(120)
  @IsOptional()
  age?: number;
}

Common Validation Decorators

import {
  IsString,
  IsNumber,
  IsBoolean,
  IsEmail,
  IsUrl,
  IsDate,
  IsArray,
  IsOptional,
  IsNotEmpty,
  MinLength,
  MaxLength,
  Min,
  Max,
  Matches,
  IsEnum,
} from 'class-validator';

export class UserDto {
  @IsString()
  @IsNotEmpty()
  @MinLength(2)
  name: string;

  @IsEmail()
  email: string;

  @IsNumber()
  @Min(0)
  @Max(100)
  score: number;

  @IsOptional()
  @IsUrl()
  website?: string;

  @IsEnum(['admin', 'user', 'guest'])
  role: string;

  @Matches(/^[A-Z]{2}$/)
  countryCode: string;
}

Global Validation Pipe

Apply validation globally in main.ts:
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,              // Strip properties that don't have decorators
      forbidNonWhitelisted: true,   // Throw error if non-whitelisted properties exist
      transform: true,              // Automatically transform payloads to DTO instances
      transformOptions: {
        enableImplicitConversion: true,  // Enable implicit type conversion
      },
    }),
  );
  
  await app.listen(3000);
}

Custom Validation

Create custom validators:
import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator';

export function IsStrongPassword(validationOptions?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      name: 'isStrongPassword',
      target: object.constructor,
      propertyName: propertyName,
      options: validationOptions,
      validator: {
        validate(value: any, args: ValidationArguments) {
          return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/.test(value);
        },
        defaultMessage(args: ValidationArguments) {
          return 'Password must be at least 8 characters with uppercase, lowercase, number, and special character';
        },
      },
    });
  };
}

// Usage
export class CreateUserDto {
  @IsString()
  @IsStrongPassword()
  password: string;
}
Tip: Always validate input to prevent security issues and bugs. Validation should happen at the controller level using DTOs and pipes.

3.5 Middleware

Middleware is executed before the route handler. Use it for logging, authentication, request transformation, CORS, and other cross-cutting concerns.

What is Middleware?

Middleware functions have access to:
  • Request object
  • Response object
  • Next function (to pass control to next middleware)
They can:
  • Execute code before/after the route handler
  • Modify request/response objects
  • End the request-response cycle
  • Call the next middleware in the stack

Functional Middleware

Simple middleware as a function:
import { Request, Response, NextFunction } from 'express';

export function logger(req: Request, res: Response, next: NextFunction) {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next();
}

Class-Based Middleware

More powerful, can inject dependencies:
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
    next();
  }
}

Middleware with Dependencies

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { ConfigService } from '../config/config.service';

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  constructor(private configService: ConfigService) {}

  use(req: Request, res: Response, next: NextFunction) {
    const apiKey = req.headers['x-api-key'];
    const validKey = this.configService.get('API_KEY');
    
    if (apiKey !== validKey) {
      return res.status(401).json({ message: 'Unauthorized' });
    }
    
    next();
  }
}

Registering Middleware

Register in module using configure method:
import { Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { LoggerMiddleware } from './middleware/logger.middleware';

@Module({})
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('users');  // Apply to all /users routes
  }
}

Route-Specific Middleware

configure(consumer: MiddlewareConsumer) {
  consumer
    .apply(LoggerMiddleware)
    .forRoutes(
      { path: 'users', method: RequestMethod.GET },
      { path: 'users/:id', method: RequestMethod.GET },
    );
}

Excluding Routes

configure(consumer: MiddlewareConsumer) {
  consumer
    .apply(LoggerMiddleware)
    .exclude(
      { path: 'users', method: RequestMethod.GET },
      'users/(.*)',
    )
    .forRoutes('*');
}

Multiple Middleware

configure(consumer: MiddlewareConsumer) {
  consumer
    .apply(LoggerMiddleware, AuthMiddleware)
    .forRoutes('*');
}

Global Middleware

Apply middleware globally in main.ts:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { logger } from './middleware/logger.middleware';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(logger);  // Global middleware
  await app.listen(3000);
}
Tip: Use middleware for cross-cutting concerns that apply to many routes. For route-specific logic, use guards or interceptors.

3.6 Guards

Guards determine whether a request should be handled by the route. They run after middleware but before the route handler. Use them for authentication and authorization.

What are Guards?

Guards have a single responsibility: determine if a request should proceed. They return:
  • true - Request proceeds
  • false - Request is denied (throws ForbiddenException)

Basic Guard

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    return Boolean(request.headers['authorization']);
  }
}

Using Guards

Controller-level:
@Controller('users')
@UseGuards(AuthGuard)
export class UsersController {
  @Get()
  findAll() {
    return [];
  }
}
Method-level:
@Controller('users')
export class UsersController {
  @Get()
  @UseGuards(AuthGuard)
  findAll() {
    return [];
  }
}
Global Guard:
// main.ts
app.useGlobalGuards(new AuthGuard());

Execution Context

The ExecutionContext provides access to:
  • Request object
  • Response object
  • Next function
  • Handler (controller method)
  • Class (controller class)
@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const response = context.switchToHttp().getResponse();
    const handler = context.getHandler();
    const controller = context.getClass();
    
    // Access request data
    const token = request.headers['authorization'];
    
    return Boolean(token);
  }
}

Role-Based Guard

import { Injectable, CanActivate, ExecutionContext, SetMetadata } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

// Decorator to set roles
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
    
    if (!requiredRoles) {
      return true;  // No roles required
    }
    
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    
    return requiredRoles.some(role => user.roles?.includes(role));
  }
}

// Usage
@Controller('users')
@UseGuards(RolesGuard)
export class UsersController {
  @Delete(':id')
  @Roles('admin')
  remove(@Param('id') id: string) {
    return this.usersService.remove(id);
  }
}

Async Guards

Guards can be async:
@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = request.headers['authorization'];
    
    if (!token) {
      return false;
    }
    
    const user = await this.authService.validateToken(token);
    request.user = user;
    
    return Boolean(user);
  }
}
Tip: Use guards for permission checks, roles, and authentication logic. They’re perfect for protecting routes and controlling access.

3.7 Interceptors

Interceptors can transform the result returned from a function, extend basic method behavior, or handle cross-cutting concerns like logging, caching, or response shaping.

What are Interceptors?

Interceptors are similar to middleware but have access to:
  • Execution context
  • Call handler (to invoke the route handler)
  • Observable streams (for async operations)
They can:
  • Execute code before/after method execution
  • Transform the result
  • Transform exceptions
  • Extend method behavior

Basic Interceptor

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Before...');
    
    const now = Date.now();
    return next.handle().pipe(
      tap(() => console.log(`After... ${Date.now() - now}ms`)),
    );
  }
}

Response Transformation Interceptor

Wrap responses in a consistent format:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map(data => ({
        success: true,
        data,
        timestamp: new Date().toISOString(),
      })),
    );
  }
}

Error Transformation Interceptor

Transform errors into consistent format:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      catchError(err => {
        return throwError(() => ({
          success: false,
          error: err.message,
          statusCode: err.status || 500,
        }));
      }),
    );
  }
}

Caching Interceptor

Cache responses:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  private cache = new Map();

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const key = request.url;
    
    if (this.cache.has(key)) {
      return of(this.cache.get(key));
    }
    
    return next.handle().pipe(
      tap(data => {
        this.cache.set(key, data);
        // Clear cache after 5 minutes
        setTimeout(() => this.cache.delete(key), 5 * 60 * 1000);
      }),
    );
  }
}

Timeout Interceptor

Add timeout to requests:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { timeout, catchError } from 'rxjs/operators';

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      timeout(5000),  // 5 second timeout
      catchError(err => {
        if (err instanceof TimeoutError) {
          return throwError(() => new RequestTimeoutException());
        }
        return throwError(() => err);
      }),
    );
  }
}

Using Interceptors

Controller-level:
@Controller('users')
@UseInterceptors(LoggingInterceptor)
export class UsersController { }
Method-level:
@Get()
@UseInterceptors(TransformInterceptor)
findAll() {
  return [];
}
Global Interceptor:
// main.ts
app.useGlobalInterceptors(new TransformInterceptor());
Tip: Use interceptors for logging, response formatting, caching, and error transformation. They’re powerful for cross-cutting concerns.

3.8 Error Handling

NestJS provides built-in exception filters and HTTP exceptions for consistent error handling.

Built-in HTTP Exceptions

import {
  BadRequestException,
  UnauthorizedException,
  NotFoundException,
  ForbiddenException,
  NotAcceptableException,
  RequestTimeoutException,
  ConflictException,
  GoneException,
  HttpVersionNotSupportedException,
  PayloadTooLargeException,
  UnsupportedMediaTypeException,
  UnprocessableEntityException,
  InternalServerErrorException,
  NotImplementedException,
  BadGatewayException,
  ServiceUnavailableException,
  GatewayTimeoutException,
} from '@nestjs/common';

@Get(':id')
findOne(@Param('id') id: string) {
  const user = this.usersService.findOne(id);
  if (!user) {
    throw new NotFoundException('User not found');
  }
  return user;
}

Custom Exception Messages

throw new BadRequestException('Invalid input');
throw new NotFoundException({ message: 'User not found', error: 'Not Found' });

Exception Filters

Create custom exception filters:
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Request, Response } from 'express';

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

    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const message =
      exception instanceof HttpException
        ? exception.getResponse()
        : 'Internal server error';

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message,
    });
  }
}
Using Exception Filters:
@Controller('users')
@UseFilters(AllExceptionsFilter)
export class UsersController { }
Global Exception Filter:
// main.ts
app.useGlobalFilters(new AllExceptionsFilter());

3.9 Best Practices

Following best practices ensures your controllers are maintainable and scalable.

Keep Controllers Thin

Controllers should only handle HTTP concerns:
// Bad: Business logic in controller
@Post()
create(@Body() dto: CreateUserDto) {
  if (dto.email.includes('@')) {
    // validation logic
  }
  // business logic
  return { ... };
}

// Good: Controller delegates to service
@Post()
create(@Body() dto: CreateUserDto) {
  return this.usersService.create(dto);
}

Use DTOs for All Input

Always validate input using DTOs:
@Post()
create(@Body() createDto: CreateUserDto) {
  return this.usersService.create(createDto);
}

Organize Routes by Feature

Group related routes in feature modules:
// users/users.controller.ts
@Controller('users')
export class UsersController { }

// orders/orders.controller.ts
@Controller('orders')
export class OrdersController { }

Use Appropriate HTTP Methods

@Get()      // Retrieve resources
@Post()     // Create resources
@Put()      // Replace resources
@Patch()    // Partially update resources
@Delete()   // Delete resources

Return Consistent Response Shapes

Use interceptors to format responses consistently:
// All responses will be: { success: true, data: ... }
@UseInterceptors(TransformInterceptor)
@Controller('users')
export class UsersController { }

Handle Errors Gracefully

Use built-in exceptions:
if (!user) {
  throw new NotFoundException('User not found');
}

Document Your API

Use Swagger for API documentation:
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';

@ApiTags('users')
@Controller('users')
export class UsersController {
  @Get()
  @ApiOperation({ summary: 'Get all users' })
  @ApiResponse({ status: 200, description: 'Returns all users' })
  findAll() {
    return this.usersService.findAll();
  }
}

Use Pipes for Transformation

@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
  // id is already a number
  return this.usersService.findOne(id);
}

3.10 Summary

You’ve learned how to build robust, maintainable APIs using controllers and routing: Key Concepts:
  • Controllers: Handle HTTP requests and responses
  • Routing: Define routes using decorators
  • DTOs: Validate and transform input data
  • Middleware: Cross-cutting concerns before route handlers
  • Guards: Authentication and authorization
  • Interceptors: Response transformation and logging
  • Error Handling: Consistent error responses
Best Practices:
  • Keep controllers thin
  • Use DTOs for validation
  • Organize routes by feature
  • Use appropriate HTTP methods
  • Handle errors gracefully
  • Document your API
Next Chapter: Learn about providers, services, repository patterns, and domain-driven design in depth.