NestJS Controllers & Routing
The Front Door of Your API
Open interactive version (quiz + challenge)Real-world analogy
Controllers are like receptionists at a hotel. When a guest (HTTP request) arrives, the receptionist checks what they want: 'Checking in? Room 204, turn right.' 'Need towels? I'll call housekeeping (service).' The receptionist doesn't clean the rooms — they route guests to the right place and relay responses.
What is it?
Controllers in NestJS are classes decorated with @Controller() that handle incoming HTTP requests. They use method decorators (@Get, @Post, etc.) to map routes, and parameter decorators (@Param, @Query, @Body) to extract data from requests. Controllers delegate business logic to services.
Real-world relevance
Every API endpoint you've ever used — GET /products, POST /orders, DELETE /cart/items/5 — is handled by a controller. Companies like Stripe, Twilio, and GitHub design their controllers around RESTful conventions that NestJS makes easy to implement.
Key points
- @Controller() Decorator — The @Controller('users') decorator marks a class as a route handler and sets the base path. All routes inside will start with /users. You can nest paths: @Controller('api/v1/users'). Without the decorator, NestJS won't recognize the class as a controller.
- HTTP Method Decorators — @Get(), @Post(), @Put(), @Patch(), @Delete() — each maps to an HTTP method. @Get('profile') handles GET /users/profile. @Post() handles POST /users. You can have multiple methods on the same path with different HTTP verbs — that's RESTful design!
- Route Parameters — @Param('id') extracts dynamic segments from the URL. GET /users/123 → id = '123'. Use @Param() without arguments to get all params as an object. Always validate params with pipes: @Param('id', ParseIntPipe) ensures id is a number.
- Query Parameters — @Query('page') extracts query string values. GET /users?page=2&limit=10 → page = '2', limit = '10'. Great for filtering, pagination, and sorting. Use @Query() to get the entire query object at once.
- Request Body — @Body() extracts the request body from POST/PUT/PATCH requests. Use with DTOs for automatic validation: @Body() dto: CreateUserDto. NestJS parses JSON automatically. @Body('name') extracts a single field from the body.
- Response Handling — By default, NestJS serializes your return value to JSON with status 200 (GET) or 201 (POST). For custom status codes, use @HttpCode(204). For custom headers, use @Header('Cache-Control', 'none'). For redirects, use @Redirect('https://example.com', 301).
- Headers & Cookies — @Headers('authorization') reads request headers. @Req() gives you the raw Express request object — but prefer specific decorators for cleaner code. Use @Res() for the raw response (but then YOU must send the response manually).
- Wildcard & Nested Routes — Use wildcards for flexible matching: @Get('ab*cd') matches abcd, abXcd, ab123cd. Group related routes with sub-paths: @Get(':id/posts') for /users/123/posts. NestJS resolves routes in order — put specific routes before wildcards.
Code example
// users.controller.ts — Complete example
import {
Controller, Get, Post, Put, Delete,
Param, Query, Body, HttpCode, Header,
ParseIntPipe, DefaultValuePipe,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
// GET /users?page=1&limit=10&search=john
@Get()
findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
@Query('search') search?: string,
) {
return this.usersService.findAll({ page, limit, search });
}
// GET /users/123
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findOne(id);
}
// POST /users (body = CreateUserDto)
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
// PUT /users/123
@Put(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto,
) {
return this.usersService.update(id, updateUserDto);
}
// DELETE /users/123
@Delete(':id')
@HttpCode(204)
remove(@Param('id', ParseIntPipe) id: number) {
return this.usersService.remove(id);
}
// GET /users/123/posts — nested route
@Get(':id/posts')
getUserPosts(@Param('id', ParseIntPipe) id: number) {
return this.usersService.getUserPosts(id);
}
}Line-by-line walkthrough
- 1. users.controller.ts — Complete example
- 2. Importing decorators from NestJS
- 3.
- 4.
- 5.
- 6. Importing the service for business logic
- 7. Importing the DTO for creating users
- 8. Importing the DTO for updating users
- 9.
- 10. Setting base route to /users
- 11. Exporting the controller class
- 12. Injecting the service via constructor
- 13.
- 14. GET /users with query parameters
- 15. Get decorator — handles GET requests
- 16. Method definition
- 17. Extract 'page' from query, default 1, convert to number
- 18. Extract 'limit' from query, default 10, convert to number
- 19. Optional search query parameter
- 20. Closing parameter list
- 21. Delegate to service with the parameters
- 22. Closing method
- 23.
- 24. GET /users/123
- 25. Get with :id route parameter
- 26. Extract id from URL, validate as integer
- 27. Delegate to service
- 28. Closing method
- 29.
- 30. POST /users — create a new user
- 31. Post decorator — handles POST requests
- 32. Post decorator defaults to 201 Created status
- 33. Extract and validate body against DTO
- 34. Delegate to service
- 35. Closing method
- 36.
- 37. PUT /users/123 — update a user
- 38. Put decorator with :id parameter
- 39. Method definition
- 40. Extract and validate the id
- 41. Extract and validate the body
- 42. Closing parameter list
- 43. Delegate to service
- 44. Closing method
- 45.
- 46. DELETE /users/123
- 47. Delete decorator with :id parameter
- 48. Set 204 No Content status
- 49. Extract and validate the id
- 50. Delegate to service
- 51. Closing method
- 52.
- 53. GET /users/123/posts — nested route
- 54. Nested route under /users/:id
- 55. Extract the user id
- 56. Delegate to service
- 57. Closing method
- 58. Closing the controller class
Spot the bug
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}
@Get()
findAll(@Query('page') page: number) {
return this.usersService.findAll(page);
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}
}Need a hint?
Query parameters and route parameters come in as strings by default. What pipe should you use to convert them?
Show answer
Both 'page' and 'id' arrive as strings, but the service expects numbers. Fix: @Query('page', ParseIntPipe) page: number and @Param('id', ParseIntPipe) id: number. Without ParseIntPipe, you'd be passing strings like '2' instead of the number 2.
Explain like I'm 5
Imagine you're at an airport check-in desk. The agent (controller) takes your info (request) — 'I want a window seat' (body) for 'flight 5' (param). The agent doesn't fly the plane — they tell the airline system (service) what you want and hand you a boarding pass (response). Controllers are the check-in agents of your API!
Fun fact
NestJS uses the same decorator pattern as Angular and Java Spring Boot. The @Controller decorator was inspired by Spring's @RestController — Kamil Myśliwiec wanted to bring that enterprise Java organization to the Node.js world, but keep it fun!
Hands-on challenge
Create a ProductsController with: GET /products (with pagination query params), GET /products/:id, POST /products, PUT /products/:id, DELETE /products/:id, and GET /products/:id/reviews. Use ParseIntPipe on all :id params. Add a @HttpCode(204) on the delete route.
More resources
- NestJS Controllers (NestJS Official)
- NestJS Route Parameters (NestJS Official)
- RESTful API Design Best Practices (RESTful API)