NestJS Day 2: What is Controller?
Abstract
This is a concise, summarized approach to learn NestJS. For more in-depth knowledge about NestJS, visit the official NestJS documentation.
What is a Controller?
Controllers are responsible for handling incoming HTTP requests and returning responses to the client. The purpose of controllers is to receive specific requests for the application. The routing mechanism controls which controller receives which requests.
Frequently, each controller has more than one route, and different routes can perform different actions. In order to create a basic controller, we use classes and decorators. Decorators associate classes with required metadata and enable Nest to create a routing map.
Controller Basics
Let’s create a basic controller using the NestJS CLI:
nest generate controller cats
This will create:
src/cats/cats.controller.ts
src/cats/cats.controller.spec.ts
Here’s what a basic controller looks like:
import { Controller, Get } from "@nestjs/common";
@Controller("cats")export class CatsController { @Get() findAll(): string { return "This action returns all cats"; }}
The @Controller('cats')
decorator defines a controller that handles routes starting with cats
. The @Get()
decorator before the findAll()
method tells Nest to create a handler for a specific endpoint for HTTP GET requests. The endpoint path is determined by concatenating the controller prefix (cats
) and any path specified in the method decorator (none in this case, so just /cats
).
When a request is made to this endpoint, Nest routes the request to our defined handler method, findAll()
. The method name is completely arbitrary and doesn’t have any special meaning in the routing decision process.
Routing
Routing in NestJS refers to how the application directs HTTP requests to controller handlers. The routing mechanism determines which controller receives which requests. Each route has a specific path and HTTP method.
Route Paths
Routes can be:
- String paths:
@Get('profile')
- Pattern paths:
@Get('ab*cd')
HTTP Methods
NestJS provides decorators for all standard HTTP methods:
@Get()@Post()@Put()@Delete()@Patch()@Options()@Head()@All() // Handles all HTTP methods
Example with Multiple Routes
Here’s an example of a controller with multiple routes:
import { Controller, Get, Post, Body } from "@nestjs/common";
@Controller("cats")export class CatsController { @Post() create(@Body() createCatDto: any): string { return "This action adds a new cat"; }
@Get() findAll(): string { return "This action returns all cats"; }
@Get(":id") findOne(@Param("id") id: string): string { return `This action returns a #${id} cat`; }}
Route Parameters
To define routes with parameters, we can add route parameter tokens in the path of the route:
@Get(':id')findOne(@Param('id') id: string): string { return `This action returns a #${id} cat`;}
Route parameters declared in this way can be accessed using the @Param()
decorator, which should be added to the method signature. The @Param()
decorator is imported from the @nestjs/common
package.
Multiple Route Parameters
You can also have multiple route parameters:
@Get(':id/details/:detailId')findOneDetail(@Param('id') id: string, @Param('detailId') detailId: string): string { return `This action returns details #${detailId} for cat #${id}`;}
Access All Parameters
To access all parameters in a route, omit the parameter name in the decorator:
@Get(':id')findOne(@Param() params: any): string { return `This action returns a #${params.id} cat`;}
Request Object
Often, we need to access the client request details. We can access the request object by instructing Nest to inject it by adding the @Req()
decorator to the handler’s signature.
import { Controller, Get, Req } from "@nestjs/common";import { Request } from "express";
@Controller("cats")export class CatsController { @Get() findAll(@Req() request: Request): string { console.log(request.url); // Access request properties console.log(request.headers); // Access request headers return "This action returns all cats"; }}
Dedicated Decorators for Request Objects
While the request object has a lot of properties, you will often only use a few of them. Nest provides dedicated decorators for common properties to extract specific properties from the request object:
Decorator | Express Equivalent |
---|---|
@Req() | req |
@Res() | res |
@Next() | next |
@Session() | req.session |
@Param(key?: string) | req.params / req.params[key] |
@Body(key?: string) | req.body / req.body[key] |
@Query(key?: string) | req.query / req.query[key] |
@Headers(name?: string) | req.headers / req.headers[name] |
@Ip() | req.ip |
@HostParam() | req.hosts |
Here’s an example using the @Body()
decorator to access the request body:
@Post()create(@Body() createCatDto: any): string { console.log(createCatDto); return 'This action adds a new cat';}
Resources (REST API)
Nest provides a way to define an entire resource with all the standard CRUD operations using the @nestjs/cli
. To generate a complete resource with CRUD operations, you can use:
nest generate resource cats
This will create:
- Controller with CRUD operations
- Service with business logic
- DTOs (Data Transfer Objects)
- Entity for database mapping
- Module to organize everything
Status Code
The response status code is always 200 by default for GET requests and 201 for POST requests. We can easily change this behavior by adding the @HttpCode()
decorator at a handler level.
import { Controller, Get, Post, HttpCode } from "@nestjs/common";
@Controller("cats")export class CatsController { @Post() @HttpCode(204) create(): string { return "This action adds a new cat"; }
@Get() findAll(): string { return "This action returns all cats"; }}
The @HttpCode()
decorator takes an HTTP status code as an argument. It’s imported from the @nestjs/common
package.
Headers
To specify a custom response header, you can use the @Header()
decorator or a library-specific response object (and call res.header()
directly).
import { Controller, Get, Header } from "@nestjs/common";
@Controller("cats")export class CatsController { @Get() @Header("Cache-Control", "none") findAll(): string { return "This action returns all cats"; }}
Redirection
To redirect a response to a specific URL, you can use the @Redirect()
decorator or a library-specific response object (and call res.redirect()
directly).
@Redirect()
takes two arguments: url
and statusCode
. Both are optional. The default value for statusCode
is 302 (Found).
import { Controller, Get, Redirect } from "@nestjs/common";
@Controller("cats")export class CatsController { @Get("docs") @Redirect("https://docs.nestjs.com", 302) getDocs() { return; // This is ignored because of @Redirect decorator }}
You can also dynamically determine the HTTP status code or redirect URL by returning an object with the shape:
{ "url": string, "statusCode": number}
The returned values will override any arguments passed to the @Redirect()
decorator. For example:
@Get('docs')@Redirect('https://docs.nestjs.com', 302)getDocs(@Query('version') version) { if (version && version === '5') { return { url: 'https://docs.nestjs.com/v5/' }; }}
Sub-Domain Routing
The @Controller
decorator can take a host
option to require that the HTTP host of the incoming requests matches some specific value.
@Controller({ host: "admin.example.com" })export class AdminController { @Get() index(): string { return "Admin page"; }}
Route Parameters in Host
Similar to route paths, the hosts option can use tokens to capture the dynamic value at that position in the host name. The host parameter tokens declared in this way can be accessed using the @HostParam()
decorator.
@Controller({ host: ":account.example.com" })export class AccountController { @Get() getInfo(@HostParam("account") account: string) { return `Account info for ${account}`; }}
Asynchronicity
Modern JavaScript extensively uses asynchronous functions. Nest supports this pattern and works well with async
functions that return a Promise.
@Get()async findAll(): Promise<any[]> { return [];}
Alternatively, you can use the RxJS Observable streams:
@Get()findAll(): Observable<any[]> { return of([]);}
Nest will automatically subscribe to the Observable and take the last emitted value once the stream is completed.
Request Payloads
When client needs to send data to the server with a POST, PUT, or PATCH request, we need to define the payload structure. NestJS uses Data Transfer Objects (DTOs) for this purpose.
First, let’s create a class to define the structure of the incoming data:
export class CreateCatDto { name: string; age: number; breed: string;}
Now we can use this DTO in our controller:
@Post()async create(@Body() createCatDto: CreateCatDto) { return 'This action adds a new cat';}
Validation with DTOs
DTOs are particularly useful when combined with validation. To add validation to our DTOs, we need to:
- Install the required packages:
npm install --save class-validator class-transformer
- Enable validation in the main.ts file:
import { ValidationPipe } from "@nestjs/common";import { NestFactory } from "@nestjs/core";import { AppModule } from "./app.module";
async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes(new ValidationPipe()); await app.listen(3000);}bootstrap();
- Add validation decorators to our DTO:
import { IsString, IsInt, Min, Max } from "class-validator";
export class CreateCatDto { @IsString() name: string;
@IsInt() @Min(0) @Max(20) age: number;
@IsString() breed: string;}
Full Resource Example
Here’s a complete example of a controller that implements a basic CRUD (Create, Read, Update, Delete) interface:
import { Controller, Get, Post, Body, Param, Put, Delete, HttpStatus, HttpCode,} from "@nestjs/common";import { CreateCatDto } from "./dto/create-cat.dto";import { UpdateCatDto } from "./dto/update-cat.dto";import { CatsService } from "./cats.service";import { Cat } from "./interfaces/cat.interface";
@Controller("cats")export class CatsController { constructor(private catsService: CatsService) {}
@Post() @HttpCode(HttpStatus.CREATED) async create(@Body() createCatDto: CreateCatDto): Promise<Cat> { return this.catsService.create(createCatDto); }
@Get() async findAll(): Promise<Cat[]> { return this.catsService.findAll(); }
@Get(":id") async findOne(@Param("id") id: string): Promise<Cat> { return this.catsService.findOne(id); }
@Put(":id") async update( @Param("id") id: string, @Body() updateCatDto: UpdateCatDto ): Promise<Cat> { return this.catsService.update(id, updateCatDto); }
@Delete(":id") @HttpCode(HttpStatus.NO_CONTENT) async remove(@Param("id") id: string): Promise<void> { return this.catsService.remove(id); }}
Associated Service
Here’s what the associated service might look like:
import { Injectable } from "@nestjs/common";import { Cat } from "./interfaces/cat.interface";import { CreateCatDto } from "./dto/create-cat.dto";import { UpdateCatDto } from "./dto/update-cat.dto";
@Injectable()export class CatsService { private readonly cats: Cat[] = [];
create(createCatDto: CreateCatDto): Cat { const cat = { id: Date.now().toString(), ...createCatDto }; this.cats.push(cat); return cat; }
findAll(): Cat[] { return this.cats; }
findOne(id: string): Cat { return this.cats.find((cat) => cat.id === id); }
update(id: string, updateCatDto: UpdateCatDto): Cat { const index = this.cats.findIndex((cat) => cat.id === id); if (index >= 0) { this.cats[index] = { ...this.cats[index], ...updateCatDto }; return this.cats[index]; } return null; }
remove(id: string): void { const index = this.cats.findIndex((cat) => cat.id === id); if (index >= 0) { this.cats.splice(index, 1); } }}
Using Express Response Object
While the standard pattern is to return a value and let Nest handle the response, you can also take full control of the response object:
import { Controller, Get, Res } from "@nestjs/common";import { Response } from "express";
@Controller("cats")export class CatsController { @Get() findAll(@Res() response: Response) { response.status(200).json({ data: [], message: "This action returns all cats", }); }}
Library-specific Mode
For compatibility with third-party libraries that require access to the native response object, or to improve performance by bypassing the Nest response handling layer, you can use the passthrough
option:
@Get()findAll(@Res({ passthrough: true }) res: Response) { res.status(HttpStatus.OK); return [];}
This will give you access to the native response object while still allowing Nest to handle the response.
Scopes
In Node.js, each request is handled by a separate thread. Therefore, it’s safe to use mutable request-level state. However, when using GraphQL with subscriptions or WebSockets, Nest controllers can be shared across multiple requests, making request-level state unsafe. To address this, you can use different injection scopes:
- DEFAULT: A single instance is shared across the entire application (singleton)
- REQUEST: A new instance is created for each incoming request
- TRANSIENT: A new instance is created for each consumer (each controller, service, etc.)
Here’s how to specify a scope for a controller:
import { Controller, Get, Scope } from "@nestjs/common";
@Controller({ path: "cats", scope: Scope.REQUEST,})export class CatsController { constructor(private catsService: CatsService) { console.log("Controller created"); }
@Get() findAll() { return this.catsService.findAll(); }}
Summary
Controllers are a fundamental part of NestJS applications, responsible for handling incoming requests and returning appropriate responses. In this article, we covered:
- Basic controller creation and routing
- Working with route parameters and request objects
- Setting response status codes and headers
- Handling redirections
- Using sub-domain routing
- Working with asynchronous operations
- Processing request payloads with DTOs
- Implementing full RESTful resources
- Controlling the request/response lifecycle
- Understanding controller scopes
In the next article, we will explore Providers and Services, which handle the business logic of your application.
To learn more about controllers, visit the official NestJS documentation on controllers.