Ibiyemi Adewakun Ibiyemi is a full-stack developer from Lagos. When she's not writing code, she likes to read, listen to music, and put cute outfits together.

Understanding controllers and routes in NestJS

9 min read 2621

Understanding Controllers And Routes In Nestjs

NestJS is a Node.js web framework built with TypeScript used to build scalable server-side applications. The server side of an application, which does not have UI elements, performs important logic like handling requests, sending responses, storing data in a database, and much more.

Controllers and routes play an important role in server-side applications. In this article, we’ll take a look at routes and controllers in NestJS. We will cover:

To follow along, you’ll need to install Node.js, a JavaScript package manager — I’m using Yarn — and an IDE or text editor of your choice, like Sublime Text or Visual Studio Code. Make sure you’re using Node v12 or newer.

A brief review of server-side (HTTP) requests

A server-side, or HTTP, request is what we refer to as a user asking the application to perform an action related to a resource. These user requests are made through a client, such as a browser or application.

When we ask our application to perform certain actions, these HTTP requests are classified by verbs. Here are a few commonly used verbs and what requests they relate to:

  • GET: Used for requests to retrieve a resource or a list of resources
    • A request to list the application’s users could look like: GET /users
    • A request to provide the details of user A could look like: GET /users/A
  • POST: Used for requests to create a resource entry
    • A request to create a new user could look like: *POST /users*
  • PUT: Used for requests to update an existing resource
    • A request to update user A’s email and phone number could look like: PUT /users/A
  • DELETE: Used for requests to delete an existing resource
    • A request to delete user A could look like: DELETE /users/A

You may have noticed that in the examples above, we referred to our resource in the plural users. There is no right or wrong way to name your resources, but this is a popular convention for representing all your resources. For a specific resource, you would follow the plural resource name with a unique identifier — for example, users/A or users/1.

These are the most frequently used HTTP verbs, but there are a few others, such as PATCH, OPTIONS, and more. You can read about all the HTTP verbs and methods in the MDN docs.

Defining NestJS routes and controllers

Now that we have reviewed HTTP requests, let’s talk about how routes and controllers are used in Nest.

A route is a combination of an HTTP method, a path, and the function to handle it defined in an application. They help determine which controllers receive certain requests.

A controller is a class defined with methods for handling one or more requests. The controller provides the handler function for a route.

Note that in NestJS, the routes are defined in the controller alongside the handler method using decorators.

Learn more about handling requests and responses with controllers and routes in Nest.

Creating our NestJS Project

We will create our project using the NestJS CLI. Let’s install it now:

$ npm i -g @nestjs/cli

Once that’s installed, we can run the Nest command to create a new project:

$ nest new fruit-tree

In this case, fruit-tree is our project name. This is a simple project that returns Tree metadata and illustrates how routes work in NestJS.

Follow the prompts given after running the Nest command. Nest will ask what you’d like to use as your package manager. I chose Yarn. And that’s it, our project is ready!

Nest creates some core files in the src/ directory of our project. Let’s go through these files:

  • app.controller.ts is a basic controller class with a single route definition
  • app.controller.spec.ts is the unit test file for the controller class app.controller.ts
  • app.module.ts this is the root module for our application. All other modules we create will be registered in here
  • app.service.ts is a basic service used by our controller to perform some logic
  • main.ts is the entry point of our application

The main.ts file is where the Nest application instance is created and registered to a port for access from our client apps.

Creating our own NestJS routes and controllers

Let’s start creating our own resources, routes, and controllers in our Nest project.



Setting up the database and ORM

Let’s set up a MySQL database and ORM for retrieving and updating data. First, we will install @nestjs/typeform, typeform, and mysql2 by running the following command:

$ yarn add @nestjs/typeorm typeorm mysql2

Now that our needed libraries are installed, let’s import and register the TypeOrmModule in our core module AppModule:

// app.module.ts

import { TypeOrmModule } from '@nestjs/typeorm';
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'xxxx',
      database: 'fruit-tree',
      entities: [],
      synchronize: true,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Once TypeOrmModule is registered in AppModule, we will not need to register it in any other modules we create. Note that you may want to set up a config like Dotenv so you don’t commit your MySQL credentials.

After registering the TypeOrmModule in our core module, we can inject DataSource and EntityManager objects from TypeOrm in other modules of our project.

Creating our Tree entity and TreeService

TypeORM uses the repository design pattern, which is a design paradigm that allows you to abstract communication with the data layer in a way that separates business logic from data objects and the type of database used to manage them.

To start, we’ll create a tree directory for our tree module. This directory will house the tree entity and, eventually, the routes, controllers, and services for tree requests:

$ cd src/
$ mkdir tree
$ cd tree/
$ touch tree.entity.ts

In our tree.entity.ts, we will specify a few simple columns and attributes:

// tree.entity.ts

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Tree {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column()
  age: number;

  @Column()
  isEndangered: boolean;
}

Let’s create our TreeService for running operations on our Tree entity:

$ touch tree.service.ts

We will inject our treeRepository in our TreeService to perform operations on the entity:

// tree.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Tree } from './tree.entity';

@Injectable()
export class TreeService {
  constructor(
    @InjectRepository(Tree)
    private treeRepository: Repository<Tree>,
  ) {}

  findAll(): Promise<Tree[]> {
    return this.treeRepository.find();
  }

  findOne(id: number): Promise<Tree> {
    return this.treeRepository.findOneBy({ id });
  }

  async deleteById(id: number): Promise<void> {
    await this.treeRepository.delete(id);
  }
}

Now we’ll create our TreeModule and register our Tree entity and TreeService:

$ touch tree.module.ts


// tree.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

import { Tree } from './tree.entity';
import { TreeService } from './tree.service';

@Module({
  imports: [TypeOrmModule.forFeature([Tree])],
  providers: [TreeService],
})
export class TreeModule {}

We will also register the TreeModule in the core module in app.module.ts:

// app.module.ts

...
import { TreeModule } from './tree/tree.module';

@Module({
  imports: [
    ...
    TreeModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Defining our routes and controllers

So far in our project, we’ve set up a MySQL database, TypeORM to handle operations to our DB, a TreeEntity class where we have defined the attributes of our data entity, and a service through which we can read or write into our data entities.

Now we will create some Nest routes, along with controller methods to handle those routes! Remember: a route is a medium through which your user can request your application to perform certain operations, while the controller is the class through which we honor those route requests.

Let’s define a route to fetch all trees in our database. First, we’ll create our controller file:

$ cd src/trees/
$ touch tree.controller.ts

In our tree.controller.ts file, we’ll import the needed decorators from @nestjs/common. We will then define our route using the intended HTTP verb decorator along with the controller method to handle that route:

// tree.controller.ts

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

import { Tree } from './tree.entity';
import { TreeService } from './tree.service';

@Controller('trees')
export class TreeController {
  constructor(private readonly treeService: TreeService) {}

  @Get()
  getTrees(): Promise<Tree[]> {
    return this.treeService.findAll();
  }
}

Note that in the @Controller decorator, we set the path prefix for all the routes that will be defined in this file. For example, the route defined in our above file is GET /trees.

Our controller method calls the findAll() method of the treeService to return a list of our tree entities.

Don’t forget to register the controller in the module:

// tree.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

import { TreeController } from './tree.controller';
import { Tree } from './tree.entity';
import { TreeService } from './tree.service';

@Module({
  imports: [TypeOrmModule.forFeature([Tree])],
  providers: [TreeService],
  controllers: [TreeController],
})
export class TreeModule {}

Passing parameters in routes

We defined a very simple route above to fetch and return a list of resources. What if we want to define a route on a specific resource item — for example, to retrieve a single item using its unique identifier? Let’s take a look!

In our controller — in this case, tree.controller.ts — we’ll import an additional decorator from @nestjs/common called Param and use it in our controller as shown below:

// tree.controller.ts

import { Controller, Get, Param, Req, Res } from '@nestjs/common';
import { Tree } from './tree.entity';
import { TreeService } from './tree.service';

@Controller('trees')
export class TreeController {
  constructor(private readonly treeService: TreeService) {}

  @Get()
  getTrees(): Promise<Tree[]> {
    return this.treeService.findAll();
  }

  @Get('/:id')
  getSingleTree(@Param() params: { id: number }): Promise<Tree> {
    return this.treeService.findOne(params.id);
  }
}

​​In the snippet above, we defined a route with the decorator @Get('/:id')​ to signify that a parameter id​ will be passed in. To access the parameter id​ inside our handler controller method, we passed the @Param()​ decorator as a parameter for our method.

To actually use the content of @Param(),​ we assigned it a variable name params​. Additionally, because it’s TypeScript, we defined the contents of params​ as an interface { id: number }​.

Now in the method, we can reference the ID using params.id. The route we’ve defined will match GET /trees/:id.

Note that in our example above, we passed in and referenced the entire param object. Another option is to only pass in the id parameter we need to the controller method. We can do this by using @Param('id') id: string and referencing just id in our controller method.

Passing a request with a payload body

We’ve defined simple routes with no parameters; we’ve defined routes with parameters. But how do we handle routes that accept a payload — for example, to create an entity with that payload? Let’s take a look.

For this example, we will define a POST route. The first step is to add Post to our import list from @nestjs/common. We’ll also import Body from @nestjs/common, through which we’ll pass the payload body to the controller method for our route.

First, we’re going to do a minor detour to keep things clean and define a DTO class. This class will define the attributes we expect in our payload body:

$ cd src/trees
$ mkdir dto
$ cd dto/
$ touch create-tree.dto.ts

In our DTO file, we need to add the following:

// create-tree.dto.ts

export class CreateTreeDTO {
  _id?: number;

  name: string;

  age: number;

  isEndangered?: boolean;
}

While defining the DTO, I decided to make isEndangered an optional field, so we will update our entity to set a default value for that attribute when none is set:

// tree.entity.ts

 ...
 @Column({ default: false })
  isEndangered: boolean;
}

Now we can use this DTO class in our route definition:

// tree.controller.ts

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

import { Tree } from './tree.entity';
import { TreeService } from './tree.service';
import { CreateTreeDTO } from './dto/create-tree.dto';

@Controller('trees')
export class TreeController {
  constructor(private readonly treeService: TreeService) {}

  ...

  @Post()
  createTree(@Body() body: CreateTreeDTO): Promise<Tree> {
    return this.treeService.create(body);
  }
}

Passing query parameters in routes

Next, let’s look at sending queries into routes. We’ll extend our list of all endpoints to accept queries with which we can filter our listing results.


More great articles from LogRocket:


To start, we’ll import Query from @nestjs/common. Then we will pass it to our controller method, like so:

// tree.controller.ts

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

import { Tree } from './tree.entity';
import { TreeService } from './tree.service';
import { CreateTreeDTO } from './dto/create-tree.dto';

@Controller('trees')
export class TreeController {
  constructor(private readonly treeService: TreeService) {}

  @Get()
  getTrees(@Query() query: { isEndangered?: boolean }): Promise<Tree[]> {
    return this.treeService.findAll({ isEndangered: query.isEndangered });
  }
  ...

Now that we’ve passed in the query option isEndangered — which, as we defined, is passed to our controller method and service method — we will extend both TreeService and TreeEntity to accept and filter results by the query option:

// tree.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

import { CreateTreeDTO } from './dto/create-tree.dto';
import { TreeFilterOptions } from './interfaces/filters';
import { Tree } from './tree.entity';

@Injectable()
export class TreeService {
  constructor(
    @InjectRepository(Tree)
    private treeRepository: Repository<Tree>,
  ) {}

  findAll(filters: TreeFilterOptions): Promise<Tree[]> {
    return this.treeRepository.find({
      where: {
        isEndangered: filters.isEndangered,
      },
    });
  }

You’ll notice we defined an interface for our filter options TreeFilterOptions that matches our route query object, and is what will be sent from the controller. Here is the interface:

// interfaces/filters.ts

export interface TreeFilterOptions {
  isEndangered?: boolean;
}

Validating route parameters

An important part of defining routes in NestJS is making sure the data sent via parameters, queries, or payload bodies match what our application expects.

To start, we will import the packages class-validator and class-transformer by running the following command:

$ yarn add class-validator class-transformer

Now let’s extend our CreateTreeDTO class for our POST /trees request to use the validator decorators available in class-validator:

// dto/tree.dto.ts

import { IsNotEmpty, IsNumber } from 'class-validator';

export class CreateTreeDTO {
  _id?: number;

  @IsNotEmpty()
  name: string;

  @IsNumber()
  age: number;

  isEndangered?: boolean;
}

In our example above, the IsNotEmpty decorator makes sure the request does not accept an empty string as a valid input even though it is technically a string of length 0. If a request is made with an empty string value, the response will match:

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": ["name must not be empty"]
}

Note that NestJS also allows us to validate route parameters using pipes.

Conclusion

In this article, we looked at how routes and controllers are defined in NestJS. We reviewed examples of HTTP requests and explored how options like payload bodies, queries, and parameters can be passed in route requests using a sample API application.

You can find the entire code used in this article in my Github repo.

I hope you found this article useful. Please share any thoughts or questions you might have in the comment section!

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Ibiyemi Adewakun Ibiyemi is a full-stack developer from Lagos. When she's not writing code, she likes to read, listen to music, and put cute outfits together.

Leave a Reply