Lesson 42 of 49 intermediate

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

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. 1. users.controller.ts — Complete example
  2. 2. Importing decorators from NestJS
  3. 3.
  4. 4.
  5. 5.
  6. 6. Importing the service for business logic
  7. 7. Importing the DTO for creating users
  8. 8. Importing the DTO for updating users
  9. 9.
  10. 10. Setting base route to /users
  11. 11. Exporting the controller class
  12. 12. Injecting the service via constructor
  13. 13.
  14. 14. GET /users with query parameters
  15. 15. Get decorator — handles GET requests
  16. 16. Method definition
  17. 17. Extract 'page' from query, default 1, convert to number
  18. 18. Extract 'limit' from query, default 10, convert to number
  19. 19. Optional search query parameter
  20. 20. Closing parameter list
  21. 21. Delegate to service with the parameters
  22. 22. Closing method
  23. 23.
  24. 24. GET /users/123
  25. 25. Get with :id route parameter
  26. 26. Extract id from URL, validate as integer
  27. 27. Delegate to service
  28. 28. Closing method
  29. 29.
  30. 30. POST /users — create a new user
  31. 31. Post decorator — handles POST requests
  32. 32. Post decorator defaults to 201 Created status
  33. 33. Extract and validate body against DTO
  34. 34. Delegate to service
  35. 35. Closing method
  36. 36.
  37. 37. PUT /users/123 — update a user
  38. 38. Put decorator with :id parameter
  39. 39. Method definition
  40. 40. Extract and validate the id
  41. 41. Extract and validate the body
  42. 42. Closing parameter list
  43. 43. Delegate to service
  44. 44. Closing method
  45. 45.
  46. 46. DELETE /users/123
  47. 47. Delete decorator with :id parameter
  48. 48. Set 204 No Content status
  49. 49. Extract and validate the id
  50. 50. Delegate to service
  51. 51. Closing method
  52. 52.
  53. 53. GET /users/123/posts — nested route
  54. 54. Nested route under /users/:id
  55. 55. Extract the user id
  56. 56. Delegate to service
  57. 57. Closing method
  58. 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

Open interactive version (quiz + challenge) ← Back to course: Full-Stack Playbook