DEV Community

Sai Swaroop Bijinapalli
Sai Swaroop Bijinapalli

Posted on

NestJS Request Lifecycle & SOLID Design Principles

1. Introduction

NestJS provides a structured request-handling mechanism that processes incoming HTTP requests through different components before returning responses to clients.

These components include Middleware, Guards, Interceptors, Pipes and Exception Filters.

Apart from request handling, NestJS applications benefit from software design principles such as the Dependency Inversion Principle (DIP) and Single Responsibility Principle (SRP).

These principles help developers build applications that are maintainable, reusable and loosely coupled.

In this blog, we will understand the NestJS request lifecycle and important SOLID design principles using practical backend examples.


2. NestJS Request Lifecycle

What is the Request Lifecycle?

The request lifecycle describes the sequence of operations performed when an HTTP request enters a NestJS application and a response is returned to the client.

For example, when a user requests an Instagram profile:

GET /users/101
Enter fullscreen mode Exit fullscreen mode

The request passes through different stages before the application sends the response.

Request Lifecycle Flow

Client Request → Middleware → Guards → Interceptors → Pipes → Controller → Service → Response

Main stages

  1. Middleware: Executes operations before the route handler pipeline.
  2. Guards: Determines whether the request is allowed to access a route.
  3. Interceptors: Executes logic before and after the route handler.
  4. Pipes: Validates and transforms incoming arguments.
  5. Controller: Handles the HTTP request.
  6. Service: Executes business logic.
  7. Exception Filters: Handle exceptions and generate appropriate error responses.

Exception filters are not a normal step in every successful request. They handle exceptions when they occur.


3. Middleware

What is Middleware?

Middleware is a function that executes during the request-response cycle before the route handler is executed.

It has access to the request object, response object and the next middleware function.

Why do we use Middleware?

  • Logging incoming requests.
  • Tracking request information.
  • Adding request metadata.
  • Processing certain headers.
  • Implementing general request-processing operations.

Example

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(
      `${req.method} ${req.originalUrl}`
    );

    next();
  }
}
Enter fullscreen mode Exit fullscreen mode

Understanding the code

  • NestMiddleware: Interface used to define NestJS middleware.
  • use(): Executes when a request reaches the middleware.
  • req: Contains incoming request information.
  • res: Represents the HTTP response.
  • next(): Passes control to the next middleware or request-processing stage.

For example, if a user sends:

GET /users/101
Enter fullscreen mode Exit fullscreen mode

The middleware logs:

GET /users/101
Enter fullscreen mode Exit fullscreen mode

4. Guards

What are Guards?

Guards are components used to determine whether a request is allowed to access a particular route.

They are commonly used for authentication and authorization.

Why do we use Guards?

Consider an Instagram application.

Suppose a user wants to access a protected profile endpoint.

GET /users/profile
Enter fullscreen mode Exit fullscreen mode

Before allowing access, the application checks whether the user has an authenticated session.

Example

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

@Injectable()
export class AuthGuard implements CanActivate {

  canActivate(context: ExecutionContext): boolean {

    const request = context
      .switchToHttp()
      .getRequest();

    return !!request.session?.userId;
  }
}
Enter fullscreen mode Exit fullscreen mode

Understanding the code

  • CanActivate: Interface implemented by guards.
  • ExecutionContext: Provides information about the current request and handler.
  • switchToHttp(): Accesses the HTTP request context.
  • getRequest(): Retrieves the request object.
  • canActivate(): Returns whether access is allowed.

If the user has a valid session containing userId, access is granted.

Otherwise, access is denied.

Applying the Guard

@UseGuards(AuthGuard)
@Get('profile')
getProfile() {
  return 'User profile';
}
Enter fullscreen mode Exit fullscreen mode

The guard checks the request before the controller handler executes.


5. Interceptors

What are Interceptors?

Interceptors are components that execute logic before and after a route handler.

They can intercept the execution of a request and process its result.

Why do we use Interceptors?

  • Logging execution time.
  • Transforming response data.
  • Implementing caching.
  • Monitoring application performance.
  • Handling common response-processing logic.

Example

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

import { Observable, tap } from 'rxjs';

@Injectable()
export class LoggingInterceptor
implements NestInterceptor {

  intercept(
    context: ExecutionContext,
    next: CallHandler
  ): Observable<any> {

    console.log('Before request handler');

    const startTime = Date.now();

    return next.handle().pipe(
      tap(() => {
        console.log(
          `Execution time: ${Date.now() - startTime}ms`
        );
      })
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Understanding the code

  • NestInterceptor: Interface used to create interceptors.
  • intercept(): Executes before the route handler.
  • next.handle(): Passes execution to the next stage and returns an Observable.
  • tap(): Performs a side effect when the response data is emitted.

An interceptor can perform operations before and after the controller handler.


6. Pipes

What are Pipes?

Pipes are components used to validate or transform incoming data before it reaches the controller method.

Why do we use Pipes?

  • Validating request parameters.
  • Converting data types.
  • Transforming incoming values.
  • Preventing invalid input from reaching business logic.

Example: ParseIntPipe

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

@Controller('users')
export class UsersController {

  @Get(':id')
  getUser(
    @Param('id', ParseIntPipe) id: number
  ) {
    return {
      userId: id
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Understanding the code

Suppose the frontend sends:

GET /users/101
Enter fullscreen mode Exit fullscreen mode

The URL parameter initially comes as a string.

ParseIntPipe converts it into a number.

If the incoming value is not a valid integer, NestJS returns a validation error.

Other Common Pipes

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseUUIDPipe
  • DefaultValuePipe

7. Exception Filters

What are Exception Filters?

Exception Filters are components used to handle exceptions and customize the error responses returned to clients.

They help manage errors in a consistent way.

Why do we use Exception Filters?

  • Handling application errors.
  • Returning appropriate HTTP status codes.
  • Customizing error messages.
  • Centralizing error-handling logic.

Example

throw new NotFoundException('User not found');
Enter fullscreen mode Exit fullscreen mode

If a requested user does not exist, NestJS can return:

{
  "statusCode": 404,
  "message": "User not found",
  "error": "Not Found"
}
Enter fullscreen mode Exit fullscreen mode

Common HTTP Exceptions

Exception HTTP Status
BadRequestException 400
UnauthorizedException 401
ForbiddenException 403
NotFoundException 404
ConflictException 409
InternalServerErrorException 500

NestJS provides built-in exception handling. Custom exception filters can be implemented when specific error formatting or handling is required.


8. Dependency Inversion Principle (DIP)

What is DIP?

The Dependency Inversion Principle is one of the five SOLID design principles.

It states that high-level modules should not depend directly on low-level modules. Both should depend on abstractions.

In simple terms, business logic should depend on a contract rather than a specific implementation.

Without DIP

Consider a service that directly creates a PostgreSQL database object.

class PostgreSQLDatabase {

  saveUser() {
    console.log('User saved');
  }
}

class UsersService {

  private database = new PostgreSQLDatabase();

  createUser() {
    this.database.saveUser();
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, UsersService is tightly coupled to PostgreSQLDatabase.

If we want to change the database implementation, the service may need modification.

Applying DIP

First, define an abstraction.

interface UserRepository {
  saveUser(): void;
}
Enter fullscreen mode Exit fullscreen mode

Then create an implementation.

class PostgreSQLDatabase
implements UserRepository {

  saveUser() {
    console.log('User saved in PostgreSQL');
  }
}
Enter fullscreen mode Exit fullscreen mode

Now the service depends on the abstraction.

class UsersService {

  constructor(
    private database: UserRepository
  ) {}

  createUser() {
    this.database.saveUser();
  }
}
Enter fullscreen mode Exit fullscreen mode

Now UsersService does not directly depend on PostgreSQLDatabase.

Benefits of DIP

  1. Reduces tight coupling.
  2. Improves flexibility.
  3. Makes unit testing easier.
  4. Supports replacing implementations.
  5. Improves maintainability.

In NestJS, dependency injection tokens and provider configurations can be used to inject different implementations of an abstraction.


9. Single Responsibility Principle (SRP)

What is SRP?

The Single Responsibility Principle is another important SOLID design principle.

It states that a class should have one responsibility and one reason to change.

Without SRP

Consider a class that handles multiple responsibilities.

class UserManager {

  createUser() {}

  deleteUser() {}

  sendEmail() {}

  generateReport() {}

  connectDatabase() {}

}
Enter fullscreen mode Exit fullscreen mode

This class manages users, sends emails, generates reports and connects to the database.

This makes the class difficult to maintain.

Applying SRP

Separate the responsibilities into different classes.

class UsersService {
  createUser() {}
  deleteUser() {}
}
Enter fullscreen mode Exit fullscreen mode
class EmailService {
  sendEmail() {}
}
Enter fullscreen mode Exit fullscreen mode
class ReportService {
  generateReport() {}
}
Enter fullscreen mode Exit fullscreen mode
class DatabaseService {
  connectDatabase() {}
}
Enter fullscreen mode Exit fullscreen mode

Now every class has a clearly defined responsibility.

SRP in a NestJS Application

Class Responsibility
UsersController Handle user HTTP requests
UsersService User business logic
PostsController Handle post HTTP requests
PostsService Post business logic
AuthService Authentication operations
PrismaService Database connectivity

Benefits of SRP

  1. Improves code readability.
  2. Makes debugging easier.
  3. Reduces complexity.
  4. Improves code reusability.
  5. Makes testing easier.

10. Practical Application in an Instagram Backend

Consider an Instagram-style backend built using NestJS, Prisma and PostgreSQL.

When a user requests their profile:

GET /users/profile
Enter fullscreen mode Exit fullscreen mode

The application may perform the following operations:

  1. Middleware logs the incoming request.
  2. AuthGuard checks the authenticated session.
  3. An interceptor records request execution time.
  4. Pipes validate incoming parameters when required.
  5. UsersController receives the request.
  6. UsersService executes the business logic.
  7. PrismaService communicates with PostgreSQL.
  8. The response is returned to the frontend.

If an exception occurs, NestJS exception handling returns an appropriate error response.

DIP helps separate the business logic from specific database implementations, while SRP ensures that each class has a clear responsibility.


11. Difference Between Middleware, Guards, Interceptors, Pipes and Filters

Component Main Responsibility
Middleware Processes incoming requests
Guards Controls access to routes
Interceptors Processes execution before and after handlers
Pipes Validates and transforms input
Exception Filters Handles exceptions

These components work together to create a structured request-processing mechanism.


12. Conclusion

NestJS provides several components to manage HTTP requests and application behavior effectively.

Middleware handles incoming request processing, Guards manage route access, Interceptors process execution and responses, Pipes validate and transform input, and Exception Filters handle errors.

Along with these components, SOLID principles such as DIP and SRP help developers write loosely coupled, maintainable and reusable code.

Understanding these concepts is important for developing scalable backend applications using NestJS.

Top comments (0)