Controllers
Controllers are the entry point for incoming requests in a NestJS application. Their job is narrow and important: receive a request, delegate the work to providers, and return a response. Keeping controllers thin, no business logic, is one of the defining habits of well-structured Nest code.
Defining a controller
A controller is a class annotated with @Controller(). The decorator’s argument becomes a route prefix shared by every handler in the class.
import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll(): string {
return 'This returns all cats';
}
}
This maps to GET /cats. The class lives in a module’s controllers array, which is how Nest discovers and instantiates it.
Route decorators
Each HTTP method has a corresponding decorator. The string passed to it is appended to the controller prefix to form the full path.
| Decorator | HTTP method | Typical use |
|---|---|---|
@Get() | GET | Read resources |
@Post() | POST | Create a resource |
@Put() | PUT | Replace a resource |
@Patch() | PATCH | Partially update |
@Delete() | DELETE | Remove a resource |
@Controller('cats')
export class CatsController {
@Get(':id') // GET /cats/42
findOne() {}
@Post() // POST /cats
create() {}
}
Extracting request data
Nest provides parameter decorators so you never touch the raw request object. Use @Param for route parameters, @Query for query strings, and @Body for the request body.
import { Controller, Get, Post, Param, Query, Body } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get(':id')
findOne(@Param('id') id: string) {
return `Cat #${id}`;
}
@Get()
search(@Query('breed') breed: string) {
return `Cats of breed ${breed}`;
}
@Post()
create(@Body() body: CreateCatDto) {
return body;
}
}
DTOs and validation
A Data Transfer Object (DTO) defines the shape of an incoming payload. Combined with class-validator decorators and Nest’s ValidationPipe, DTOs give you declarative, type-safe request validation.
First enable the global pipe in main.ts:
app.useGlobalPipes(
new ValidationPipe({ whitelist: true, transform: true }),
);
whitelist: truestrips any property not declared on the DTO;transform: trueconverts plain payloads into DTO class instances and coerces primitives. Always enable both, they prevent over-posting attacks and remove manual casting.
Then describe and constrain the payload:
import { IsString, IsInt, Min, IsOptional } from 'class-validator';
export class CreateCatDto {
@IsString()
name: string;
@IsInt()
@Min(0)
age: number;
@IsOptional()
@IsString()
breed?: string;
}
A request that violates a rule is rejected automatically:
Output:
POST /cats { "name": "Milo", "age": -3 }
400 Bad Request
{
"statusCode": 400,
"message": ["age must not be less than 0"],
"error": "Bad Request"
}
A complete CRUD controller
Putting it together, a controller delegates all real work to an injected service:
import {
Controller, Get, Post, Patch, Delete,
Param, Body, HttpCode,
} from '@nestjs/common';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { UpdateCatDto } from './dto/update-cat.dto';
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Get()
findAll() {
return this.catsService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.catsService.findOne(id);
}
@Post()
create(@Body() dto: CreateCatDto) {
return this.catsService.create(dto);
}
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateCatDto) {
return this.catsService.update(id, dto);
}
@Delete(':id')
@HttpCode(204)
remove(@Param('id') id: string) {
return this.catsService.remove(id);
}
}
Best Practices
- Keep controllers thin. They should orchestrate, never implement business logic, push that into providers.
- Always validate input with DTOs and a global
ValidationPipe; never trust the raw body. - Use precise return types and status codes. Apply
@HttpCode()where the default does not match REST semantics (e.g.204for deletes). - One resource per controller. Resist the urge to bolt unrelated endpoints onto a controller; create a new feature module instead.
- Prefer DTOs over inline types so validation, transformation, and OpenAPI documentation stay in one place.