Skip to content

nest_03_controllers - Controllers & Request Handling

Controllers are the dumbest part of your app They should be thin , brainless , and only concerned with routing HTTP to the right service If your controller has business logic , you're doing it wrong and your code will rot faster than a potato in a SOC break room

what's in here

  • @Controller decorator and route prefixes
  • Route decorators: @Get , @Post , @Put , @Delete , @Patch
  • Request object decorators: @Body , @Param , @Query , @Headers , @Req
  • Response handling: status codes , headers , redirects
  • Async controllers and Observable returns

basic controller structure

import { Controller, Get, Post, Body, Param } from '@nestjs/common'
import { UsersService } from './users.service'
import { CreateUserDto } from './dto/create-user.dto'

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

  @Get()
  async findAll(): Promise<User[]> {
    return this.usersService.findAll()
  }

  @Get(':id')
  async findOne(@Param('id') id: string): Promise<User> {
    return this.usersService.findOne(id)
  }

  @Post()
  async create(@Body() createUserDto: CreateUserDto): Promise<User> {
    return this.usersService.create(createUserDto)
  }
}

@Controller('users') prefixes all routes with /users The constructor injects UsersService through DI - no new keyword , no manual instantiation Each method maps to an HTTP verb via decorators

route decorators and patterns

@Controller('products')
export class ProductsController {
  @Get()                    // GET /products
  findAll() {}

  @Get(':id')               // GET /products/123
  findOne(@Param('id') id: string) {}

  @Post()                   // POST /products
  create(@Body() body: any) {}

  @Put(':id')               // PUT /products/123
  update(@Param('id') id: string, @Body() body: any) {}

  @Delete(':id')            // DELETE /products/123
  remove(@Param('id') id: string) {}

  @Patch(':id')             // PATCH /products/123
  partialUpdate(@Param('id') id: string, @Body() body: any) {}
}

Route parameters use :param syntax - same as Express Nest extracts them and passes them through @Param() You can also access the raw Express Request object with @Req() but avoid it unless you absolutely need something not exposed by Nest decorators

request object decorators

import { Controller, Get, Post, Req, Body, Param, Query, Headers, Ip } from '@nestjs/common'
import { Request } from 'express'

@Controller('api')
export class ApiController {
  @Get()
  handleRequest(
    @Req() req: Request,                 // full Express request object
    @Body() body: any,                   // parsed request body
    @Param('id') id: string,             // route parameter
    @Query('page') page: number,         // query string parameter
    @Query() query: any,                 // all query parameters
    @Headers('authorization') auth: string, // specific header
    @Headers() headers: any,             // all headers
    @Ip() ip: string,                    // client IP address
  ) {
    return { id, page, ip }
  }
}

@Req() exposes the raw Express request - use it sparingly @Body() , @Param() , @Query() are your daily drivers @Ip() is useful for rate limiting and audit logs - grab it at the controller level instead of parsing headers yourself

custom parameter decorators

import { createParamDecorator, ExecutionContext } from '@nestjs/common'

export const CurrentUser = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest()
    return request.user  // set by auth guard middleware
  }
)

// usage in controller
@Get('profile')
getProfile(@CurrentUser() user: User) {
  return user
}

Custom decorators reduce repetition when you need the same data from multiple endpoints Auth guards set request.user , then @CurrentUser() extracts it without controllers knowing about request internals

response handling

Nest automatically serializes returned objects to JSON Set status codes explicitly for non-200 responses:

import { Controller, Post, HttpCode, HttpStatus, Header, Redirect, Res } from '@nestjs/common'
import { Response } from 'express'

@Controller('orders')
export class OrdersController {
  @Post()
  @HttpCode(HttpStatus.CREATED)  // 201
  @Header('X-Custom', 'value')   // custom response header
  async create() {
    return { id: 'order-123' }
  }

  @Get('redirect')
  @Redirect('https://docs.nestjs.com', 301)
  redirectDoc() {
    // optionally return to override redirect
    // return { url: 'https://other.com', statusCode: 302 }
  }

  // using Express response directly (avoid when possible)
  @Get('legacy')
  legacyRoute(@Res() res: Response) {
    // when you use @Res() , Nest disables its response handling
    // you must call res.send() yourself
    res.status(200).json({ message: 'legacy response' })
  }
}

@HttpCode() overrides the default 200 status Nest sends 201 for POST by convention but you should set it explicitly @Header() adds custom headers without touching the response object Avoid @Res() unless you absolutely need Express-native response control - it disables Nest's interceptors and response mapping for that endpoint

error handling in controllers

Controllers should catch errors? No Let errors propagate to exception filters (covered in nest_10_filters) Use exceptions from Nest instead:

import { Controller, Get, Param, NotFoundException, BadRequestException } from '@nestjs/common'

@Controller('users')
export class UsersController {
  @Get(':id')
  async findOne(@Param('id') id: string) {
    if (!isValidUUID(id)) {
      throw new BadRequestException('invalid user ID format')
    }

    const user = await this.usersService.findOne(id)
    if (!user) {
      throw new NotFoundException('user not found')
    }

    return user
  }
}

BadRequestException returns 400 with { statusCode: 400, message: 'invalid user ID format', error: 'Bad Request' } NotFoundException returns 404 Your exception filter will catch these and format them consistently - more in nest_10_filters

security note: input at controller boundary

The controller is your first line of defense Validate every parameter here or in a pipe before it reaches your service Rule of thumb: if a service method receives bad data , the controller should have caught it

// bad - service gets raw unvalidated input
@Post()
async create(@Body() body: any) {
  return this.usersService.create(body)  // might pass SQL injection
}

// good - validation happens before service
@Post()
async create(@Body() createUserDto: CreateUserDto) {
  return this.usersService.create(createUserDto)  // already validated
}

CreateUserDto with class-validator decorators makes this clean Pipes section (nest_09_pipes) covers validation in depth

prerequisites

nest_02_get_started - App Bootstrap & CLI Deep Dive


next -> nest_04_providers - Providers & Dependency Injection