Skip to content
NestJS core 3 min read

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.

DecoratorHTTP methodTypical use
@Get()GETRead resources
@Post()POSTCreate a resource
@Put()PUTReplace a resource
@Patch()PATCHPartially update
@Delete()DELETERemove 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: true strips any property not declared on the DTO; transform: true converts 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. 204 for 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.
Last updated June 1, 2026
Was this helpful?