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