NestJS Response Handling
NestJS gives you two approaches for sending responses: the standard approach (automatic) and the library-specific approach (manual). The standard approach handles most situations and requires the least code. The manual approach gives you full control when you need it. This topic covers both, explains when to use each, and shows how to customize status codes, headers, and response shapes.
The Standard (Automatic) Approach
When you return a value from a controller method, NestJS serializes it and sends it as the response body. You write nothing extra:
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findOne(id);
// NestJS automatically: serializes the object, sets Content-Type: application/json,
// sets status 200, and sends the response
}
NestJS handles these automatically:
- Strings → sent as plain text (200)
- Objects and arrays → serialized to JSON (200)
- Promises → awaited, then the resolved value is sent
- Observables (RxJS) → subscribed to, then the emitted value is sent
Customizing the Status Code
Use @HttpCode() to override the default status code for a method:
@Post()
@HttpCode(201) // default for POST, shown for clarity
create(@Body() dto: CreateUserDto) { ... }
@Delete(':id')
@HttpCode(204) // 204 = No Content (no body returned)
remove(@Param('id', ParseIntPipe) id: number) { ... }
Adding Custom Headers
The @Header() decorator adds a header to every response from that method:
@Get()
@Header('X-Custom-Header', 'my-value')
@Header('Cache-Control', 'no-store')
findAll() {
return this.usersService.findAll();
}
Redirects
Use @Redirect() to send a redirect response:
@Get('old-path')
@Redirect('/new-path', 301)
redirectToNew() {}
To redirect dynamically (based on logic inside the method), return an object with url and statusCode:
@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs(@Query('version') version: string) {
if (version === 'v10') {
return { url: 'https://docs.nestjs.com/v10' };
}
}
When the method returns a redirect object, it overrides the URL in the @Redirect() decorator.
Throwing HTTP Exceptions
When something goes wrong, throw one of NestJS's built-in HTTP exceptions. NestJS catches it and sends the correct error response automatically:
import { NotFoundException, BadRequestException } from '@nestjs/common';
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
const user = this.usersService.findOne(id);
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
Common NestJS Exception Classes: Exception | HTTP Status | Use When ---------------------------|-------------|--------------------------- BadRequestException | 400 | Invalid input UnauthorizedException | 401 | Not logged in ForbiddenException | 403 | Logged in, no permission NotFoundException | 404 | Resource does not exist ConflictException | 409 | Duplicate resource InternalServerErrorException | 500 | Unexpected server error
Response Diagram for Error Handling
GET /users/999
|
v
UsersController.findOne(999)
|
v
usersService.findOne(999) → returns null
|
v
throw new NotFoundException('User with ID 999 not found')
|
v
NestJS catches the exception
|
v
Response: HTTP 404
{
"statusCode": 404,
"message": "User with ID 999 not found",
"error": "Not Found"
}
The Library-Specific (Manual) Approach
When you need full control over the response — setting cookies, streaming files, or using Express-specific features — inject the Express response object using @Res():
@Get()
findAll(@Res() res: Response) {
const users = this.usersService.findAll();
return res.status(200).json(users);
}
Use the manual approach sparingly. It bypasses NestJS's response serialization and makes interceptors and automatic exception handling stop working for that route. Mixing both approaches in the same method is not supported.
Passthrough Mode
If you need the Express response object to set a cookie but still want NestJS to handle serialization, use passthrough mode:
@Get()
findAll(@Res({ passthrough: true }) res: Response) {
res.cookie('session', 'abc123');
return this.usersService.findAll(); // NestJS still handles this
}
Passthrough mode gives you access to the raw response object without disabling NestJS's automatic response handling. This is the preferred pattern when you need to do something beyond what decorators alone support.
Consistent Response Shapes
Many APIs wrap every response in a standard envelope for consistency:
{
"status": "success",
"data": { ... },
"timestamp": "2025-01-15T10:00:00.000Z"
}
NestJS interceptors handle this transformation cleanly across all routes without modifying every controller method individually. Interceptors are covered in a dedicated topic later in this course.
