Understanding Nest.js controllers

Chamuditha Kekulawala
5 min readJun 27, 2024

--

In the previous article, we set up a new Nest project and ran the application. Now let’s talk about controllers.

Controllers are responsible for handling incoming requests and returning responses to the client:

A controller’s purpose is to receive specific requests for the application. The routing mechanism controls which controller receives which requests. Usually, each controller has more than one route, and different routes can perform different actions.

In order to create a basic controller, we use the @Controller() decorator. Decorators associate classes with required metadata and enable Nest to create a routing map (tie requests to the corresponding controllers):

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}

@Get()
getHello(): string {
return this.appService.getHello();
}
}

This is the basic controller you get when you load a new Nest project. The @Get() HTTP request method decorator before the getHello() method tells Nest to create a handler for a specific endpoint for HTTP requests. The endpoint corresponds to the HTTP request method (in this case GET) and the route path.

What is the route path? The route path for a handler is determined by concatenating the (optional) prefix declared for the controller, and any path specified in the method’s decorator. In the above controller we don’t have a prefix, so Nest will map only GET requests to this handler.

Routing

Using a path prefix in a @Controller() decorator allows us to easily group a set of related routes, and minimize repetitive code. For example, we may choose to group a set of routes that manage interactions with a cat entity under the route /cats. In that case, we could specify the path prefix cats in the @Controller() decorator so that we don't have to repeat that portion of the path for each route in the file:

import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
@Get()
findAllCats(): string {
return 'This action returns all cats';
}
}

Since we’ve declared a prefix for every route ( cats), and haven't added any path information in the decorator, Nest will map GET /cats requests to this handler. We could also include any path string declared in the request method decorator. For example, a path prefix of cats combined with the decorator @Get('breed') would produce a route mapping for requests like GET /cats/breed.

Request object

Handlers often need access to 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()
findAllCats(@Req() request: Request): string {
return 'This action returns all cats';
}
}

We’ll typically also want to provide an endpoint that creates new records. For this, let’s create the POST handler:

import { Controller, Get, Post } from '@nestjs/common';

@Controller('cats')
export class CatsController {
@Post()
createCat(): string {
return 'This action adds a new cat';
}
@Get()
findAllCats(): string {
return 'This action returns all cats';
}
}

It’s that simple! Nest provides decorators for all of the standard HTTP methods: @Get(), @Post(), @Put(), @Delete(), @Patch(), @Options(), and @Head(). In addition, @All() defines an endpoint that handles all of them.

Redirection

To redirect a response to a specific URL, you can either use a @Redirect() decorator. @Redirect() takes two optional arguments, url and statusCode. The default value of statusCode is 302 (Found) if omitted.

@Get()
@Redirect('https://cats.com', 301)

Route parameters

Routes with static paths won’t work when you need to accept dynamic data as part of the request (e.g., GET /cats/1 to get cat with id 1). In order to define routes with parameters, we can add route parameter tokens in the path of the route to capture the dynamic value at that position in the request URL. The route parameter token in the @Get() decorator example below demonstrates this usage:

@Get(':id')
findOneCat(@Param() params: any): string {
console.log(params.id);
return `This action returns a #${params.id} cat`;
}

Route parameters declared in this way can be accessed using the @Param() decorator, which makes the route parameters available as properties of that decorated method parameter inside the body of the method. In the code above, we can access the id parameter by referencing params.id.

You can also pass in a particular parameter token to the decorator, and then reference the route parameter directly by name in the method body.

@Get(':id')
findOneCat(@Param('id') id: string): string {
return `This action returns cat with id: #${id}`;
}

Request payloads

In the POST route handler, we can accept any client paramsadding the @Body() decorator here. But first, we need to determine the DTO (Data Transfer Object) schema.

A DTO is an object that defines how the data will be sent over the network. We could determine the DTO schema by using TypeScript interfaces, or by simple classes.

Interestingly, Nest.js recommends using classes here. Why? Classes are part of the JavaScript ES6 standard, and therefore they are preserved as real entities in the compiled JavaScript. On the other hand, since TypeScript interfaces are removed during the transpilation, Nest can’t refer to them at runtime. This is important because features such as Pipes enable additional possibilities when they have access to the metatype of the variable at runtime.

Let’s create the CreateCatDto class:

export class CreateCatDto {
name: string;
age: number;
breed: string;
}

We can use this newly created DTO inside the CatsController:

@Post()
async createCat(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat';
}

Full example

Below is an example that makes use of several of the available decorators to create a basic controller:

import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common';
import { CreateCatDto, UpdateCatDto, ListAllEntities } from './dto';

@Controller('cats')
export class CatsController {
@Post()
async createCat(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat';
}
@Get()
findAllCats(@Query() query: ListAllEntities) {
return `This action returns all cats (limit: ${query.limit} items)`;
}
@Get(':id')
findOneCat(@Param('id') id: string) {
return `This action returns a #${id} cat`;
}
@Put(':id')
updateCat(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
return `This action updates a #${id} cat`;
}
@Delete(':id')
removeCat(@Param('id') id: string) {
return `This action removes a #${id} cat`;
}
}

Notice that the CreateCat function is asynchronous. Nest supports and works well with async functions.

Including Controllers on Modules

With the above controller fully defined, Nest still doesn’t know that CatsController exists and as a result won't create an instance of this class.

Controllers always belong to a module, which is why we include the controllers array within the @Module() decorator. Since we haven't yet defined any other modules except the root AppModule, we'll use that to introduce the CatsController:

import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';

@Module({
controllers: [CatsController],
})
export class AppModule {}

We attached the metadata to the module class using the @Module() decorator, and Nest can now easily reflect which controllers have to be mounted.

That’s all about controllers! In the nest part we’ll talk about providers and services. Thanks for reading 🎉

--

--