Mastering NestJs :The Framework for Scalable Backend Applications

Mastering NestJs :The Framework for Scalable Backend Applications

The world of backend development is vast, with many frameworks vying for attention. One standout is NestJS—a progressive Node.js framework for building scalable, reliable, and maintainable server-side applications. In this introductory blog, we’ll explore what makes NestJS special and why it’s an essential tool for modern backend developers.

Why Choose NestJS?

NestJS embraces TypeScript by default, ensuring type safety and better code
maintainability. It’s built on Express (or Fastify for faster performance), adding powerful tools and an opinionated structure that promotes clean code practices. Inspired by Angular’s architecture, NestJS leverages decorators, dependency injection, and modules, making it easy to build complex systems with minimal boilerplate.
NestJS brings structure to the often chaotic world of JavaScript development. Here are its key benefits:

  • Modular Architecture: Helps separate application features into reusable modules.

  • TypeScript Support: Provides type safety, making applications easier to scale and maintain.

  • Dependency Injection (DI): Simplifies managing services and dependencies across your application.

  • Built-in Support for Middleware, Guards, Pipes, and Interceptors: Makes handling requests, validation, and transformations a breeze.

NestJS is ideal for building complex enterprise-grade applications, microservices, and REST or GraphQL APIs.

Setting Up a NestJS Project

Let’s walk through creating a basic NestJS project.

Install the NestJS CLI

The NestJS CLI simplifies project creation and management.

npm install -g @nestjs/cli
Create a New Project
nest new hello-world-api

This command generates a project scaffold with all the necessary boilerplate code.

Understand the Project Structure
hello-world-api/
├── src/
│   ├── app.controller.ts      // Handles incoming requests
│   ├── app.service.ts         // Contains business logic
│   ├── app.module.ts          // Main application module
│   └── main.ts                // Application entry point
├── package.json
├── tsconfig.json
└── nest-cli.json

Exploring the Core Files

main.ts
This file is the application’s entry point. It bootstraps the application using NestFactory.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

app.module.ts
Modules are the backbone of a NestJS application.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

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

app.controller.ts
Controllers handle HTTP requests.

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();
  }
}

app.service.ts
Services contain the business logic and are injected into controllers.

import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello, NestJS!';
  }
}

Running the Application

Use the following command to start your server:

npm run start

Visit localhost:3000 in your browser to see the output "Hello, NestJS!".

Understanding Controllers, Services, and Modules in NestJS

In NestJS, the core of your application revolves around controllers, services, and modules. These components work together to handle requests, encapsulate business logic, and maintain a clean structure. Let’s break down each concept and see how they interact.

Controllers: Routing and Handling Requests

Controllers in NestJS define how your application responds to incoming requests. A controller is a class with route handlers, each mapped to a specific HTTP method and path.

Here’s a simple CarsController example:

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

@Controller('cars')
export class CarsController {
  @Get()
  findAll(): string {
    return 'Returning all cars';
  }
}

By visiting http://localhost:3000/cars, the findAll method returns a list of cars. Each method is decorated with HTTP method decorators (@Get(), @Post(), etc.) to define the route’s behavior.

What is a Controller?
Controllers are responsible for handling HTTP requests and returning responses. A controller uses decorators like @Controller() and @Get() to map routes to methods.

Services: Business Logic Layer

Services handle the application’s logic and data access. In NestJS, they are injectable classes marked with the @Injectable() decorator. This promotes dependency injection, making code more testable and maintainable.

What is a Service?
Services contain the core business logic of the application. They’re decorated with @Injectable() to allow dependency injection into controllers.

Creating a Cars Service
nest generate service cars

This creates a cars.service.ts file:

import { Injectable } from '@nestjs/common';

@Injectable()
export class CarsService {
  getAllCars(): string[] {
    return ['Toyota', 'Honda', 'Ford'];
  }
}
Injecting Services into Controllers
import { CarsService } from './cars.service';

@Controller('cars')
export class CarsController {
  constructor(private readonly carsService: CarsService) {}

  @Get()
  getAllCars(): string[] {
    return this.carsService.getAllCars();
  }
}

Here, CarsService is injected into CarsController using dependency injection. This ensures better separation of concerns.

Modules: The Glue that Binds Everything

Modules organize your application by grouping related components (controllers, services, etc.). Every NestJS application has a root module (AppModule).

What is a Module?
Modules are the backbone of a NestJS application. They help organize the code into cohesive blocks of functionality. By default, every application has a root module, typically named AppModule.

Creating a Custom Module

We can create new modules using the CLI. Let’s create a module for handling car-related logic in our Used Car Pricing API:

nest generate module cars

This generates a new folder and a cars.module.ts file:

import { Module } from '@nestjs/common';
@Module({
  imports: [],
  controllers: [],
  providers: [],
})
export class CarsModule {}

Why Use Modules?

  • They group related components (controllers, services, and providers).

  • They enhance modularity, making your application more maintainable.

  • Modules can be easily reused and imported into other modules.

What is Dependency Injection?

Dependency Injection (DI) is a design pattern used to manage dependencies in an application. Instead of creating dependencies manually, DI allows injecting them into classes. This promotes loose coupling and makes it easier to manage dependencies.


Example Without Dependency Injection

class CarsService {
  private logger = new Logger(); // Manually created
}

With Dependency Injection

@Injectable()
class CarsService {
  constructor(private readonly logger: Logger) {}
}

In the second example, Logger is injected into CarsService, making it more testable and scalable. NestJS handles dependency injection automatically when using decorators like @Injectable() and @Inject().


Understanding Providers in NestJS

A Provider is any class that can be injected into another class. Services, repositories, and custom helpers are common examples.

Creating a Custom Logger Provider

import { Injectable } from '@nestjs/common';

@Injectable()
export class CustomLogger {
  log(message: string) {
    console.log(`[CustomLogger] ${message}`);
  }
}
Registering a Provider in a Module
@Module({
  providers: [CustomLogger],
})
export class CarsModule {}
Injecting a Provider into a Service
@Injectable()
export class CarsService {
  constructor(private readonly customLogger: CustomLogger) {}

  logCarActivity() {
    this.customLogger.log('Car activity logged.');
  }
}

Built-in and Custom Providers

NestJS provides several built-in providers, but you can also create custom providers for specific use cases.

Example of Using a Value-Based Provider

const CarBrandProvider = {
  provide: 'CarBrand',
  useValue: ['Toyota', 'Ford', 'Honda'],
};

@Module({
  providers: [CarBrandProvider],
})
export class CarsModule {}

Injecting the Custom Provider

@Injectable()
export class CarsService {
  constructor(@Inject('CarBrand') private readonly carBrands: string[]) {}

  getCarBrands(): string[] {
    return this.carBrands;
  }
}

Scope of Providers – Singleton, Request, and Transient

By default, providers are singleton. However, you can change their scope using the @Injectable({ scope: Scope.REQUEST }) decorator for request-scoped or transient providers.


Benefits of Dependency Injection

  • Maintainability: Decoupling dependencies simplifies refactoring.

  • Testability: Dependencies can be mocked for testing.

  • Scalability: Complex systems are easier to manage.

When building scalable and maintainable backend applications, controlling the flow of requests and responses is a crucial aspect. NestJS offers a powerful set of tools—Middleware, Guards, Interceptors, and Pipes—that enable developers to manage incoming requests, apply validations, and enhance application logic efficiently. In this blog, we’ll take a closer look at each of these features and see how they contribute to a clean, organized, and secure application structure.

Middleware – Pre-Processing the Request Pipeline

Middleware is the gatekeeper of your application’s request pipeline. It’s the first stop for incoming requests and allows you to apply logic before they reach the controller. Common use cases for middleware include logging, request body modification, and authentication checks.

What is Middleware?
Middleware in NestJS is a function that executes before a request reaches the controller. It is typically used for logging, authentication, and modifying requests.

Creating Middleware
nest generate middleware logger

This creates a logger.middleware.ts file:

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... ${req.method} ${req.url}`);
    next(); // Pass control to the next middleware or route handler
  }
}
Applying Middleware

In app.module.ts:

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

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

Guards – Controlling Route Access

Guards are responsible for determining whether a request can proceed to the route handler. They are perfect for implementing authentication and authorization mechanisms. In NestJS, guards return a boolean value—true allows the request, while false denies it.

What is a Guard?
Guards determine whether a request is allowed to proceed. They are often used for authentication and role-based authorization.

Creating a Guard
nest generate guard auth

This creates an auth.guard.ts file:

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    return request.headers.authorization === 'valid_token';
  }
}
Using a Guard in a Controller
import { UseGuards } from '@nestjs/common';
import { AuthGuard } from './auth.guard';

@Controller('cars')
export class CarsController {
  @UseGuards(AuthGuard)
  @Get()
  getAllCars() {
    return 'Cars list, only accessible with valid token';
  }
}

Interceptors – Transforming and Logging Responses

Interceptors provide a way to transform responses or perform additional actions before and after a request is handled. They are useful for adding custom logic like logging response times or formatting data.

What is an Interceptor?
Interceptors handle transforming data or logging before sending a response. They can also measure execution time.

Creating an Interceptor
nest generate interceptor logging

This creates a logging.interceptor.ts file:

import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} 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`)),
    );
  }
}
Applying an Interceptor
@UseInterceptors(LoggingInterceptor)
@Get()
getAllCars() {
  return 'Logging interceptor in action';
}

Pipes – Validating and Transforming Input

Pipes are an integral part of NestJS for handling input validation and data transformation. They can transform request parameters or throw errors if validation fails.

What is a Pipe?
Pipes are responsible for data validation and transformation before the request reaches the handler.

Creating a Validation Pipe
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';

@Injectable()
export class ParseIntPipe implements PipeTransform {
  transform(value: string, metadata: ArgumentMetadata) {
    const val = parseInt(value, 10);
    if (isNaN(val)) {
      throw new Error('Validation failed: not a number');
    }
    return val;
  }
}
Using a Pipe in a Controller
@Get(':id')
getCarById(@Param('id', new ParseIntPipe()) id: number) {
  return `Car ID: ${id}`;
}

Summary of Use Cases

ToolPurposeExample Use Case
MiddlewarePre-process requestsLogging, modifying headers
GuardsRestrict access based on conditionsAuthentication, roles
InterceptorsTransform or log responsesResponse formatting, timing
PipesValidate and transform inputsData validation

Conclusion

NestJS is a powerful, opinionated framework that brings structure and scalability to backend development. In this blog, we explored its modular architecture, core concepts like controllers, services, and modules, and how dependency injection promotes maintainability. These foundations enable you to build robust, testable applications with minimal overhead. With this knowledge, you are well-equipped to tackle real-world projects. In the final hands-on section, we applied these principles to create a Used Car Pricing API, demonstrating how NestJS transforms ideas into production-ready solutions. Keep learning, keep experimenting, and let NestJS be your go-to framework for modern, scalable backend systems.