LearnAI ToolsCareerPractice BuildsPlayDevelopmentContact
Lesson 420 min read

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.

What You Will Learn
  • 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' }];
}
}
GET /products

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

DecoratorHTTP MethodTypical Use
@Get()GETRead one or many resources.
@Post()POSTCreate a new resource.
@Put()PUTReplace a resource completely.
@Patch()PATCHUpdate part of a resource.
@Delete()DELETERemove a resource.
@All()AnyHandle 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}`;
}
}
GET /products/42

Click Run to see what this code prints.

Params Are Always Strings

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 };
}
GET /products?category=books&page=2

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.ts
export 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) // 204
remove(@Param('id') id: string) {
// delete the product...
}
@Get('export')
@Header('Cache-Control', 'no-store')
exportAll() {
return [];
}
Avoid @Res() Unless Necessary

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

Avoid These 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.

Next Lesson →

Providers & Dependency Injection