Controllers & Routing
Build REST endpoints with NestJS controllers: route decorators, route parameters, query strings, request bodies, status codes and response handling.
On this page 14 sections
Introduction
Controllers are the front door of your API. Every HTTP request that reaches your Nest application is routed to a method on a controller, which reads the request data, calls the right service and returns a response.
- How controllers and route decorators map URLs to methods.
- How to read route parameters, query strings and request bodies.
- How to build a complete set of CRUD endpoints.
- How to control status codes and headers.
- How Nest handles async (Promise-returning) handlers.
What is a Controller?
A controller is a class decorated with @Controller(). The string you pass becomes a route prefix shared by every method in the class:
import { Controller, Get } from '@nestjs/common';
@Controller('products')export class ProductsController { @Get() findAll() { return [{ id: 1, name: 'Laptop' }, { id: 2, name: 'Phone' }]; }}Click Run to see what this code prints.
When a handler returns an object or array, Nest automatically serializes it to JSON. When it returns a primitive such as a string, Nest sends it as-is.
Route Decorators
| Decorator | HTTP Method | Typical Use |
|---|---|---|
| @Get() | GET | Read one or many resources. |
| @Post() | POST | Create a new resource. |
| @Put() | PUT | Replace a resource completely. |
| @Patch() | PATCH | Update part of a resource. |
| @Delete() | DELETE | Remove a resource. |
| @All() | Any | Handle every HTTP method on a path. |
Each decorator accepts an optional path that is appended to the controller prefix — @Get('featured') inside @Controller('products') handles GET /products/featured.
Route Parameters
Declare a dynamic segment with a colon, then read it with @Param():
import { Controller, Get, Param } from '@nestjs/common';
@Controller('products')export class ProductsController { @Get(':id') findOne(@Param('id') id: string) { return `Product #${id}`; }}Click Run to see what this code prints.
Everything that arrives in a URL is a string, even if your TypeScript type says number. Use ParseIntPipe — @Param('id', ParseIntPipe) id: number — to convert and validate it. Pipes are covered in depth in lesson 6.
Query Parameters
@Get()findAll(@Query('category') category?: string, @Query('page') page = '1') { return { category, page };}Click Run to see what this code prints.
Request Body
Use @Body() to read the JSON body of POST, PUT and PATCH requests. The standard Nest approach is to describe the expected shape with a DTO (Data Transfer Object) class:
// src/products/dto/create-product.dto.tsexport class CreateProductDto { name: string; price: number;}
// src/products/products.controller.ts@Post()create(@Body() createProductDto: CreateProductDto) { return { message: 'Product created', product: createProductDto };}Why a class rather than an interface? TypeScript interfaces disappear when code is compiled, but classes still exist at runtime — which lets Nest validate incoming data against them, as you will see in lesson 6.
A Complete CRUD Controller
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post,} from '@nestjs/common';import { ProductsService } from './products.service.js';import { CreateProductDto } from './dto/create-product.dto.js';import { UpdateProductDto } from './dto/update-product.dto.js';
@Controller('products')export class ProductsController { constructor(private readonly productsService: ProductsService) {}
@Post() create(@Body() dto: CreateProductDto) { return this.productsService.create(dto); }
@Get() findAll() { return this.productsService.findAll(); }
@Get(':id') findOne(@Param('id', ParseIntPipe) id: number) { return this.productsService.findOne(id); }
@Patch(':id') update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateProductDto) { return this.productsService.update(id, dto); }
@Delete(':id') remove(@Param('id', ParseIntPipe) id: number) { return this.productsService.remove(id); }}Notice the controller contains no business logic at all — it only reads request data and delegates to ProductsService. You will write that service in the next lesson.
Status Codes & Headers
By default Nest responds with 200 OK, except POST handlers, which respond with 201 Created. Override this with @HttpCode(), and add response headers with @Header():
import { Delete, Header, HttpCode, HttpStatus, Param } from '@nestjs/common';
@Delete(':id')@HttpCode(HttpStatus.NO_CONTENT) // 204remove(@Param('id') id: string) { // delete the product...}
@Get('export')@Header('Cache-Control', 'no-store')exportAll() { return [];}You can inject the raw Express response object with @Res(), but doing so switches that handler into "manual mode" — Nest stops sending the return value for you. Prefer returning values and using @HttpCode()/@Header(); if you really need @Res(), use @Res({ passthrough: true }) to keep Nest's standard handling.
Async Handlers
Real handlers usually wait on a database or another API. Nest fully supports async handlers — just return a Promise (or an RxJS Observable) and Nest resolves it before sending the response:
@Get()async findAll(): Promise<Product[]> { return this.productsService.findAll();}Common Mistakes
- Declaring @Get(':id') before @Get('featured') in the same controller — routes match in order, so "featured" gets treated as an id.
- Treating a route param as a number without ParseIntPipe, then comparing it with === to a numeric id.
- Writing database queries and business rules directly inside controller methods.
- Injecting @Res() and then also returning a value, which leaves the request hanging.
Best Practices
- Keep controllers thin: read input, call a service, return the result.
- Use plural, noun-based route prefixes (products, users) and let HTTP methods express the action.
- Describe every request body with a DTO class.
- Return the correct status code — 201 for created, 204 for no content, 404 for missing resources.
Frequently Asked Questions
Call app.setGlobalPrefix('api') in main.ts. Every route then starts with /api, for example /api/products.
Use @Headers('authorization') authHeader: string in the handler's parameters.
Yes — @Controller(['products', 'items']) accepts an array, though one prefix per controller is clearer in most apps.
Key Takeaways
- @Controller('prefix') groups routes; @Get/@Post/@Put/@Patch/@Delete map methods to HTTP verbs.
- @Param(), @Query() and @Body() read route params, query strings and request bodies.
- DTO classes describe the shape of request bodies.
- POST returns 201 by default, everything else 200 — change it with @HttpCode().
- Handlers can be async; Nest resolves Promises and Observables automatically.
Summary
You can now build complete REST endpoints with Nest controllers. The controller you wrote delegates to a ProductsService — next, you will learn how providers work and how Nest's dependency injection creates and delivers them automatically.