Controllers (NestJS Core Series 01)

Deepak Mandal
7 min readApr 2, 2024

--

Introduction

NestJS is a progressive Node.js framework for building efficient, reliable, and scalable server-side applications. It is built with TypeScript and leverages the power of Express (or optionally, Fastify) under the hood. One of the key components of NestJS is the controller, which is responsible for handling incoming requests and returning responses to the client. In this article, we will delve into the concept of controllers in NestJS, explore their core functionalities, and examine best practices for using them effectively.

Overview of NestJS

NestJS is designed to provide a robust framework for building server-side applications. It follows a modular architecture, which encourages the development of reusable and maintainable components. Here are some key features of NestJS:

  • TypeScript First: NestJS is built with TypeScript, offering strong typing and modern JavaScript features.
  • Modular Architecture: Encourages the development of modular and reusable components.
  • Extensible: Integrates easily with other libraries and frameworks.
  • Built-in Support for Testing: Provides tools for unit and integration testing.
  • Rich Ecosystem: Includes a wide range of modules for various functionalities, such as validation, configuration, and more.

Understanding Controllers in NestJS

What is a Controller?

In NestJS, a controller is a class that handles incoming HTTP requests and returns responses to the client. Controllers are decorated with the @Controller decorator, which marks them as controllers and allows NestJS to recognize them. Each controller can define multiple routes, each corresponding to a specific endpoint.

The Role of Controllers

Controllers in NestJS serve several critical functions:

  1. Routing: Controllers define endpoints and handle routing for different HTTP methods (GET, POST, PUT, DELETE, etc.).
  2. Request Handling: They process incoming requests, extract necessary parameters, and call appropriate services or business logic.
  3. Response Management: Controllers formulate responses and send them back to the client.
  4. Middleware Integration: They can integrate with middleware for pre-processing requests.
  5. Error Handling: Controllers can implement error handling mechanisms to manage exceptions and return meaningful error messages.

Creating Controllers

Setting Up a NestJS Project

Before creating controllers, we need to set up a NestJS project. Follow these steps to get started:

  1. Install NestJS CLI:
npm install -g @nestjs/cli

2. Create a New Project:

nest new my-nestjs-app

3. Navigate to the Project Directory:

cd my-nestjs-app

Creating a Basic Controller

To create a basic controller, use the NestJS CLI:

nest generate controller cats

This command creates a new controller named CatsController with a basic structure. Let's examine the generated code:

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

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

Decorators in Controllers

NestJS uses decorators extensively to define metadata for classes and their members. Here are some commonly used decorators in controllers:

  • @Controller(): Marks a class as a controller and optionally specifies a route prefix.
  • @Get(), @Post(), @Put(), @Delete(): Define routes for different HTTP methods.
  • @Param(): Extracts route parameters.
  • @Query(): Extracts query parameters.
  • @Body(): Extracts the request body.
  • @Headers(): Extracts request headers.

Routing with Controllers

Route Parameters

Route parameters allow you to capture values from the URL. For example:

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

@Controller('cats')
export class CatsController {
@Get(':id')
findOne(@Param('id') id: string): string {
return `This action returns a cat with ID: ${id}`;
}
}

In this example, the :id part of the route is a parameter that can be accessed using the @Param() decorator.

Query Parameters

Query parameters are used to pass data in the URL query string. For example:

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

@Controller('cats')
export class CatsController {
@Get()
findAll(@Query('age') age: string): string {
return `This action returns all cats of age: ${age}`;
}
}

Request Body

The @Body() decorator is used to extract the request body in POST and PUT requests. For example:

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

@Controller('cats')
export class CatsController {
@Post()
create(@Body() createCatDto: CreateCatDto): string {
return 'This action adds a new cat';
}
}

Here, CreateCatDto is a Data Transfer Object (DTO) that defines the shape of the request body.

Advanced Features

Middleware Integration

Middleware functions are executed before the route handler. They can be used for logging, authentication, and more. To use middleware in NestJS:

  1. Create a Middleware:
import { Injectable, NestMiddleware } from '@nestjs/common'; 
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Request...');
next();
}
}

2. Apply Middleware to Routes:

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; 
import { CatsController } from './cats.controller';
import { LoggerMiddleware } from './logger.middleware';

@Module({ controllers: [CatsController], })
export class CatsModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes(CatsController);
}
}

Guards

Guards are used to determine whether a request should be handled by the route handler. They are often used for authentication and authorization. For example:

  1. Create a Guard:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; 
import { Observable } from 'rxjs';

@Injectable() export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext ): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return validateRequest(request);
}
}

function validateRequest(request: any): boolean {
// Logic to validate the request return true;
}

2. Apply Guard to Routes:

import { Controller, Get, UseGuards } from '@nestjs/common'; 
import { AuthGuard } from './auth.guard';

@Controller('cats') export class CatsController {

@Get()
@UseGuards(AuthGuard)
findAll(): string {
return 'This action returns all cats';
}
}

Interceptors

Interceptors are used to bind extra logic before or after method execution. They can modify the incoming request or outgoing response. For example:

  1. Create an Interceptor:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(map(data => ({ data })));
}
}

2. Apply Interceptor to Routes:

import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { TransformInterceptor } from './transform.interceptor';

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

Filters

Filters are used for exception handling. They can catch exceptions thrown during request processing and return a custom response. For example:

  1. Create an Exception Filter:
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
const status = exception.getStatus();

response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}

2. Apply Filter to Routes:

import { Controller, Get, UseFilters } from '@nestjs/common';
import { HttpExceptionFilter } from './http-exception.filter';

@Controller('cats')
export class CatsController {
@Get()
@UseFilters(HttpExceptionFilter)
findAll(): string {
throw new HttpException('Forbidden', 403);
}
}

Dependency Injection in Controllers

Services and Providers

In NestJS, services are used to encapsulate business logic and interact with data sources. They are marked with the @Injectable decorator and can be injected into controllers and other providers.

Injecting Services into Controllers

To inject a service into a controller, follow these steps:

  1. Create a Service:
nest generate service cats

This command creates a new service named CatsService. Let's examine the generated code:

import { Injectable } from '@nestjs/common';  
@Injectable()
export class CatsService {

findAll(): string {
return 'This action returns all cats';
}
}

2. Inject Service into Controller:

import { Controller, Get } from '@nestjs/common'; 
import { CatsService } from './cats.service';

@Controller('cats') export class CatsController {

constructor(private readonly catsService: CatsService) {}

@Get()
findAll(): string {
return this.catsService.findAll();
}
}

In this example, the CatsController injects the CatsService and uses it to handle the findAll route.

Testing Controllers

Unit Testing

Unit testing focuses on testing individual components in isolation. NestJS provides tools for unit testing controllers using Jest. To unit test a controller:

  1. Set Up Testing Module:
import { Test, TestingModule } from '@nestjs/testing';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

describe('CatsController', () => {
let catsController: CatsController;
let catsService: CatsService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [CatsController],
providers: [CatsService],
}).compile();

catsService = module.get<CatsService>(CatsService);
catsController = module.get<CatsController>(CatsController);
});

describe('findAll', () => {
it('should return an array of cats', async () => {
const result = ['test'];
jest.spyOn(catsService, 'findAll').mockImplementation(() => result);

expect(await catsController.findAll()).toBe(result);
});
});
});

Integration Testing

Integration testing involves testing multiple components together. NestJS provides tools for integration testing using Jest and Supertest. To perform integration testing:

  1. Set Up Testing Module:
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { CatsModule } from './../src/cats/cats.module';

describe('CatsController (e2e)', () => {
let app: INestApplication;

beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [CatsModule],
}).compile();

app = moduleFixture.createNestApplication();
await app.init();
});

it('/cats (GET)', () => {
return request(app.getHttpServer())
.get('/cats')
.expect(200)
.expect('This action returns all cats');
});
});

Best Practices

Structuring Controllers

  • Single Responsibility Principle: Each controller should handle a single responsibility. Avoid putting too much logic in controllers; delegate to services.
  • Modular Structure: Organize controllers into modules. Each module should encapsulate related functionality.
  • Descriptive Naming: Use descriptive names for controllers and routes. This improves readability and maintainability.

Error Handling

  • Centralized Error Handling: Use filters for centralized error handling. This ensures consistency and simplifies maintenance.
  • Meaningful Error Messages: Provide meaningful error messages to help clients understand what went wrong.
  • Validation: Validate request data using pipes or validation libraries like class-validator.

Security Considerations

  • Authentication and Authorization: Implement authentication and authorization guards to protect sensitive routes.
  • Input Sanitization: Sanitize and validate input data to prevent injection attacks.
  • Rate Limiting: Implement rate limiting to prevent abuse and protect against denial-of-service (DoS) attacks.

Conclusion

Controllers are a fundamental component of NestJS, responsible for handling incoming requests, processing them, and returning responses. They provide a structured way to manage routing, request handling, and response management. By leveraging decorators, dependency injection, and advanced features like middleware, guards, interceptors, and filters, developers can build robust and maintainable applications. Following best practices ensures that controllers are well-organized, secure, and efficient. With the knowledge gained from this guide, you are well-equipped to create and manage controllers in your NestJS applications effectively.

--

--