Mastering NestJS: Building Robust Applications with Core Concepts

Mastering NestJS: Building Robust Applications with Core Concepts

NestJS is a powerful framework that allows building various types of applications, including APIs, microservices, and standalone apps. It utilizes modules, controllers, providers, middleware, guards, interceptors, pipes, and exception filters to manage the request handling flow effectively. This blog will dive into the core concepts of NestJS, providing examples to help you understand how to leverage its features for building robust applications.

Key Concepts of NestJS

Modules: The Building Blocks

Modules are the fundamental building blocks of a Nest application, forming a graph structure where modules can be nested within each other. A module is a class annotated with a @Module decorator, which organizes related components such as controllers, providers, and services.

// src/app.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';

@Module({
  imports: [UsersModule],
})
export class AppModule {}

Controllers: Handling Requests

Controllers in NestJS handle incoming requests and generate responses. They define routes and methods for request handling, enabling the creation of endpoints for various operations.

// src/users/users.controller.ts
import { Controller, Get } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  findAll() {
    return this.usersService.findAll();
  }
}

Providers: Dependency Injection

Providers in NestJS are classes that can be injected as dependencies into other classes. This facilitates code organization and reusability.

// src/users/users.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
  private readonly users = ['John Doe', 'Jane Doe'];

  findAll() {
    return this.users;
  }
}

Middleware: Controlling Request Flow

Middleware in NestJS can log incoming requests and control request flow stages. Middleware functions can execute before the route handler is invoked.

// src/common/middleware/logger.middleware.ts
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();
  }
}

// src/app.module.ts
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';

@Module({
  // ...
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('*');
  }
}

Guards: Security Checks

Guards in NestJS act as security checks, determining if requests meet specified conditions like roles or permissions before reaching the root handler.

// src/common/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';

@Injectable()
export class RolesGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return user && user.roles && user.roles.includes('admin');
  }
}

// src/app.module.ts
import { APP_GUARD } from '@nestjs/core';
import { RolesGuard } from './common/guards/roles.guard';

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ],
})
export class AppModule {}

Interceptors: Managing Request and Response

Interceptors provide full control over request and response cycles, allowing tasks like logging, caching, and data mapping before and after the root handler.

// src/common/interceptors/logging.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Before...');
    const now = Date.now();
    return next
      .handle()
      .pipe(
        tap(() => console.log(`After... ${Date.now() - now}ms`)),
      );
  }
}

// src/app.module.ts
import { APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor } from './common/interceptors/logging.interceptor';

@Module({
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: LoggingInterceptor,
    },
  ],
})
export class AppModule {}

Pipes: Validating and Transforming Data

Pipes in NestJS validate and transform data before it reaches the handler. This ensures data meets predefined criteria for processing.

// src/common/pipes/validation.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    if (typeof value !== 'string') {
      throw new BadRequestException('Validation failed');
    }
    return value.toUpperCase();
  }
}

// src/app.module.ts
import { APP_PIPE } from '@nestjs/core';
import { ValidationPipe } from './common/pipes/validation.pipe';

@Module({
  providers: [
    {
      provide: APP_PIPE,
      useClass: ValidationPipe,
    },
  ],
})
export class AppModule {}

Exception Filters: Handling Errors

Exception filters catch and handle errors from various parts of request handling, providing a centralized way to manage errors and ensure consistent error responses.

// src/common/filters/http-exception.filter.ts
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 response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

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

// src/app.module.ts
import { APP_FILTER } from '@nestjs/core';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
  ],
})
export class AppModule {}

Conclusion

NestJS provides a robust framework for building scalable and maintainable applications. By understanding and utilizing its core concepts — modules, controllers, providers, middleware, guards, interceptors, pipes, and exception filters — you can effectively manage the request handling flow and create applications that are versatile and easy to maintain. These examples demonstrate how to apply these concepts in real-world scenarios, helping you to craft powerful and efficient NestJS applications.