0%
June 5, 2025

Fundamentals of Nestjs

nestjs

Life Cycle of Filters, Guards, Interceptors, Pipes

Loading Environment Variables

At the app module we import the ConfigModule:

import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  ...,
  imports: [
      ...,
      ConfigModule.forRoot({
        isGlobal: true,
        load: [
          () => {
            const dotenv = require('dotenv');
            const fs = require('fs');

            const localEnv: DotenvResult = dotenv.config({ path: '.env.local' });
            if (localEnv.error) {
              console.error('Error loading .env.local:', localEnv.error);
            }

            const internalEnvPath = '.env.local.internal';
            let internalEnv: DotenvResult = { parsed: {} };

            if (fs.existsSync(internalEnvPath)) {
              internalEnv = dotenv.config({ path: internalEnvPath });
              if (internalEnv.error) {
                console.error(
                  'Error loading .env.local.internal:',
                  internalEnv.error,
                );
              }
            } else {
              console.warn('.env.local.internal file not found');
            }

            return {
              ...localEnv.parsed,
              ...internalEnv.parsed,
            };
          },
        ],
      })
      ...
  ]
})

Dependency Injection

Mimicing @Bean which injects return value of a method
@Module({
  providers: [OtherService],
  exports: [OtherService]
})
export class OtherModule {}

// some.module.ts
@Module({
  imports: [OtherModule],  // Import for non-global dependency
  providers: [
    {
      provide: 'SOME_SERVICE',
      useFactory: (
        configService: ConfigService,  // Global dependency
        otherService: OtherService     // Non-global dependency
      ) => {
        return new SomeService(
          configService.get('SOME_KEY'),
          otherService
        );
      },
      inject: [ConfigService, OtherService]
    }
  ]
})

Now we can access the return value by using @Inject("SOME_SERVICE") in the constructor.

Standard Commands to Memorize

nest g mo <path>          # module
nest g co <path> --flat   # controller
nest g s <path> --flat    # service

Swagger

configSwagger(app: INestApplication)

The following config a basic swagger doucmentation at localhost:<port>/api:

import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

function configSwagger(app: INestApplication<any>) {
    const config = new DocumentBuilder()
        .setVersion('1.0')
        .setTitle('File Generation API')
        .setDescription('File generation API with base URL at http://localhost:5090')
        .addServer('http://localhost:5090')
        .build();

    const document = SwaggerModule.createDocument(app, config);

    SwaggerModule.setup('api', app, document);
}
injectAuthLogicIntoSwagger(app: NestExpressApplication)

Now the following inject custom logic to swagger-ui-init.js which intercepts all the request to the swagger documment page.

import { NestExpressApplication } from '@nestjs/platform-express';

export function injectAuthLogicIntoSwagger(app: NestExpressApplication<any>) {
    app.use('/api', (req, res, next) => {
        if (
            req.url.includes('swagger-ui-init.js') ||
            req.url.includes('swagger-initializer.js')
        ) {
            const originalSend = res.send;
            res.send = function (data) {
                const modifiedData = data.replace(
                    'window.ui = ui',
                    `window.ui = ui
          
  // Custom request interceptor
  ui.getConfigs().requestInterceptor = (request) => {
    const token = localStorage.getItem('bearer_token');
    if (token) {
      request.headers['Authorization'] = \`Bearer \${token}\`;
    }
    return request;
  };

  // Custom response interceptor  
  ui.getConfigs().responseInterceptor = (response) => {
    if (response.url.includes('/auth/login')) {
      try {
        const responseBody = JSON.parse(response.text);
        console.log("responseBodyresponseBody", responseBody)
        if (responseBody.success && responseBody.result && responseBody.result.accessToken) {
          const token = responseBody.result.accessToken;
          localStorage.setItem('bearer_token', token);
          
          const bearerAuth = {
            bearerAuth: {
              name: "Authorization",
              schema: { type: "http", scheme: "bearer" },
              value: token
            }
          };
          
          setTimeout(() => {
            ui.authActions.authorize(bearerAuth);
          }, 100);
        }
      } catch (e) {
        console.error('Error processing login response:', e);
      }
    }
    return response;
  };

  // Auto-authorize on page load if token exists
  const storedToken = localStorage.getItem('bearer_token');
  if (storedToken) {
    const bearerAuth = {
      bearerAuth: {
        name: "Authorization",
        schema: { type: "http", scheme: "bearer" },
        value: storedToken
      }
    };
    
    setTimeout(() => {
      ui.authActions.authorize(bearerAuth);
    }, 500);
  }`,
                );
                originalSend.call(this, modifiedData);
            };
        }
        next();
    });
}

What it does:

  • When auth/login was requested, then a response of the type

    {
      result: {
        accessToken: string
      }
    }

    will be returned from our login endpoint, and we save response.result.accessToken into local storage.

  • For any other request, we check to see if bearer_token was found, we attach our request with authorization header with value Bearer <token> once it exists.

  • In this way we can test JWT authenticated endpoints easily.

Apply these to nextjs app

Now in main.ts we write

async function bootstrap() {
    const app = await NestFactory.create<NestExpressApplication>(AppModule, {
        cors: true,
    });
    app.enableCors({
        origin: '*',
        methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'],
        allowedHeaders: '*',
        credentials: false, // Set to false when using origin: "*"
        optionsSuccessStatus: 200,
        preflightContinue: false,
    });
    ...
    injectAuthLogicIntoSwagger(app);
    configSwagger(app);
    ...
    await app.listen(process.env.PORT ?? 5090).then(() => {
        console.log('Listening on: http://localhost:5090');
    });
}

Guards and Middleware

Guards and Middlewares are very similar, but they do have distinctive difference:

Key differences
// Guard - Authentication/Authorization
@Injectable()
export class AuthGuard implements CanActivate {
    canActivate(context: ExecutionContext): boolean {
        // Good for:
        // - Authentication
        // - Authorization
        // - Role checking
        // - Permission validation
        return true;
    }
}

// Middleware - Request Processing
@Injectable()
export class LoggingMiddleware implements NestMiddleware {
    use(req: Request, res: Response, next: Function) {
        // Good for:
        // - Logging
        // - Request parsing
        // - Adding headers
        // - CORS
        console.log(`${req.method} ${req.url}`);
        next();
    }
}

1. Purpose.

  • Middleware: Request processing and modification

  • Guards: Authentication and authorization

2. Capabilities.

  • Middleware: Can modify request/response

  • Guards: Can stop request flow, throw exceptions

3. Context.

  • Middleware: Limited to request/response

  • Guards: Full access to ExecutionContext

4. Dependency Injection.

  • Middleware: Limited (unless class-based)

  • Guards: Full support

5. Exception Handling.

  • Middleware: Manual handling

  • Guards: Integrated with exception filters

Guard
Create a guard boilerplate

To create a guard, let's execute

nest g guard <module-name>/guards/some --no-spec

Here it is intended not to pass --flat as we are going to create some directory and create a some.guard.ts file in it. Now this will create a boilerplate for us:

import { Request } from 'express';

export class SomeGuard implements CanActivate {
  canActive(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean>
} {
  // const request = context.switchToHttp().getRequest<Request>();
  return true;
}

The highlighted provides us a standard express.Request object to which w can do almost anything such as injecting the user object (after token-verification) as if it is an middleware:

Use the guard
@Controller('admin')
@UseGuards(JwtGuard)      // Will have access to ConfigService and UserService
export class AdminController {
    // ...
    @UseGuards(JwtGuard)  // Method level
    async createUser(...) {
      ...
    }
}
Practical example: JwtGuard and @RequestUser for controller
  • JwtGuard

    import {
        Injectable,
        CanActivate,
        ExecutionContext,
        UnauthorizedException,
    } from '@nestjs/common';
    import { verify } from 'jsonwebtoken';
    import { Request } from 'express';
    import { ConfigService } from '@nestjs/config';
    import { JwtTokenPayload } from '../types/JwtTokenPayload';
    
    @Injectable()
    export class JWTGuard implements CanActivate {
        constructor(private readonly configService: ConfigService) {}
    
        canActivate(context: ExecutionContext): boolean {
            const request = context.switchToHttp().getRequest<Request>();
            const token = this.extractTokenFromHeader(request);
    
            if (!token) {
                throw new UnauthorizedException();
            }
    
            try {
                const decoded = verify(
                    token,
                    this.configService.get('JWT_SECRET') || '',
                ) as JwtTokenPayload;
                decoded.accessToken = token;
                request.user = decoded;
                return true;
            } catch (error) {
                console.error('JWT decode error:', JSON.stringify(error));
                throw new UnauthorizedException('Invalid token');
            }
        }
    
        private extractTokenFromHeader(request: Request): string | undefined {
            const authHeader = request.headers.authorization;
            if (!authHeader) return undefined;
    
            const [type, token] = authHeader?.split(' ') || [];
            return type === 'Bearer' ? token : undefined;
        }
    }
  • @RequestUser

    import { createParamDecorator, ExecutionContext } from '@nestjs/common';
    
    export const RequestUser = createParamDecorator((data: string | undefined, ctx: ExecutionContext) => {
        const request = ctx.switchToHttp().getRequest();
    
        if (!request.user) {
            return null;
        }
    
        if (data) {
            return request.user[data];
        }
    
        return request.user;
    });
Middleware
Create a middleware boilerplate

We execute

nest g middleware logger common/middleware

to create a middleware, which is of the form

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

The middleware can be applied at both controller level or method level:

@Controller('users')
@UseMiddleware(LoggerMiddleware)  // Applied to all routes in this controller
export class UsersController {
    @Get()
    @UseMiddleware(LoggerMiddleware)  // Applied to specific route
    getUsers() {
        return 'users';
    }
}

Filter that acts as ControllerAdvice

GlobalExceptionFilter
import {
    ExceptionFilter,
    Catch,
    ArgumentsHost,
    HttpStatus,
    HttpException,
} from '@nestjs/common';
import { Response } from 'express';

export class BaseException extends HttpException {
    constructor(
        params: {
            message: string,
            status: HttpStatus
        }
    ) {
        const { message, status = HttpStatus.BAD_REQUEST } = params;
        const response: ErrorResponse = {
            statusCode: status,
            message,
        };
        super(response, status);
    }
} 

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
    private readonly logger = new Logger(GlobalExceptionFilter.name);

    catch(exception: any, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<Response>();

        let errorMessage = 'An error occurred';
        let httpStatus: HttpStatus = HttpStatus.BAD_REQUEST;

        if (exception instanceof BaseException) {
            errorMessage = exception.message;
            httpStatus = exception.getStatus();
        }

        // Extract error message from different exception types
        else if (exception instanceof HttpException) {
            const exceptionResponse = exception.getResponse();
            errorMessage =
                typeof exceptionResponse === 'string'
                    ? exceptionResponse
                    : (exceptionResponse as any).message || exception.message;
        }
        // normal error
        else if (exception instanceof Error) {
            errorMessage = exception.message;
        }

        this.logger.error(errorMessage);

        response.status(httpStatus).json({
            success: false,
            errorMessage: errorMessage,
        });
    }
}
Register the GlobalExceptionFilter

Now in main.ts let's write

app.useGlobalFilters(new GlobalExceptionFilter());

Customer Tag that Works with Guards

Define custom tag
import { SetMetadata } from "@nestjs/common"

export const Auth = (...authTypes: string[]) => {
  SetMetadata("SOME_KEY", authTypes)
}
  • Now by using @Auth to annotate a method, the SetMetadata will be executed first, injecting a value(s) that can be processed by the guard via ExecutionContext.

  • Based on this value our guard can behave differently.

Extract value provided by custom tag in guards
export class SomeGuard implements CanActivate {
  constructor(
    private readonly reflector: Reflector
  )

  canActive(context: ExecutionContext): boolean {
    const authType = this.reflector.getAllAndOverride("SOME_KEY", [
      context.getHandler(),
      context.getClass()
    ]) || []
  }
}

Now we can create an annotation to unprotect a route within a @UseGuards(JwtGuard) protected controller easily.

Interceptors

For Transactions
Decorator to set metadata
// transaction.decorator.ts
import { SetMetadata } from '@nestjs/common';
import MetaDataKey from './MetaDataKey';

export const Transactional = () => {
    return SetMetadata(MetaDataKey.TRANSACTION_KEY, true);
};
Interceptor to handle the "before" (to inject entity manager) and "after" (to rollback changes) behaviour of a function
import {
    Injectable,
    NestInterceptor,
    ExecutionContext,
    CallHandler,
    Logger,
} from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { DataSource } from 'typeorm';
import MetaDataKey from '../decorators/MetaDataKey';
import { firstValueFrom } from 'rxjs';
import { Reflector } from '@nestjs/core';

@Injectable()
export class TransactionInterceptor implements NestInterceptor {
    private readonly logger = new Logger(TransactionInterceptor.name);

    constructor(
        private readonly dataSource: DataSource,
        private readonly reflector: Reflector,
    ) { }

    async intercept(
        context: ExecutionContext,
        next: CallHandler,
    ): Promise<Observable<any>> {
        const request = context.switchToHttp().getRequest();
        const handler = context.getHandler();
        const classRef = context.getClass();

        this.logger.debug(
            `Checking transaction metadata for ${classRef.name}.${handler.name}`,
        );

        const isTransactional = this.reflector.get<boolean>(
            MetaDataKey.TRANSACTION_KEY,
            handler,
        );
  • Since decorator @Transactional at the controller method level will be executed before interceptor, we can determine if a controller method requires an entity manager for a transaction at the lighted lines.

  • These lines get the variable we set at @Transactional.

        if (!isTransactional) {
            return next.handle();
        }

        const queryRunner = this.dataSource.createQueryRunner();
        await queryRunner.connect();
        await queryRunner.startTransaction();

        // Store the transaction manager in the request
        request.transactionManager = queryRunner.manager;

        try {
            const result = await firstValueFrom(next.handle());
            await queryRunner.commitTransaction();
            return of(result);
        } catch (error) {
            await queryRunner.rollbackTransaction();
            throw error;
        } finally {
            await queryRunner.release();
        }
    }
}

Note that once @Transactional() was annotated to a method, the global interceptor above will set an entity manager into the request object, which can be resolved by annotation the following annotation at the argument:

ParamDecorator to get EntityManager from Request Object
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { EntityManager } from 'typeorm';

export const TransactionManager = createParamDecorator(
    (data: unknown, ctx: ExecutionContext): EntityManager | undefined => {
        const request = ctx.switchToHttp().getRequest();
        return request.transactionManager;
    },
); 
Exmaple of using @Transactional annotation
@Transactional()
@Post('/some-object')
async createObj(
    @RequestUser() user: JwtTokenPayload,
    @TransactionManager() em: EntityManager, 
    // Since we have @Transactional, 
    // EntityManager becomes available
) {
    await this.exportTempalteAppService.createObject(user, em);
    return new SuccessDto(null);
}

Now for transaction to function normally:

  • We replace all SomeTableRepository.someMethod(...args) by em.someMethod(SomeTable, ...args) within our application service.
Register the interceptor globally

Within boostrap let's add

import { INestApplication } from '@nestjs/common';
import { TransactionInterceptor } from '../../../common/interceptors/transaction.interceptor';
import { DataSource } from 'typeorm';
import { Reflector } from '@nestjs/core';

const boostrap = (app: INestApplication) => {
    ...
    app.useGlobalInterceptors(
        new TransactionInterceptor(app.get(DataSource), app.get(Reflector)),
    );
}
For Request Logging (especially failed ones)
import {
    Injectable,
    NestInterceptor,
    ExecutionContext,
    CallHandler,
    Logger,
} from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
    private readonly logger = new Logger(LoggingInterceptor.name);

    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        const request = context.switchToHttp().getRequest();
        const { method, url, body, headers } = request;
        const now = Date.now();

        // Log request details with more information
        this.logger.log(
            `Incoming Request: ${method} ${url}`,
            {
                headers: this.sanitizeHeaders(headers),
                body: this.sanitizeBody(body),
                contentType: headers['content-type'],
                debug: {
                    hasBody: !!body,
                    bodyType: body ? typeof body : 'undefined',
                    bodyKeys: body ? Object.keys(body) : []
                }
            },
        );

        return next.handle().pipe(
            tap({
                next: (responseBody) => {
                    const response = context.switchToHttp().getResponse();
                    const delay = Date.now() - now;

                    // Log both successful and unsuccessful responses
                    if (responseBody && responseBody.success === false) {
                        this.logger.warn(
                            `Unsuccessful Response: ${method} ${url} ${response.statusCode} - ${delay}ms`,
                            {
                                response: responseBody,
                                request: {
                                    body: this.sanitizeBody(body),
                                    method,
                                    url
                                }
                            },
                        );
                    } else {
                        this.logger.log(
                            `Outgoing Response: ${method} ${url} ${response.statusCode} - ${delay}ms`,
                            {
                                response: responseBody,
                                request: {
                                    body: this.sanitizeBody(body),
                                    method,
                                    url
                                }
                            },
                        );
                    }
                },
                error: (error) => {
                    const response = context.switchToHttp().getResponse();
                    const delay = Date.now() - now;
                    this.logger.error(
                        `Error Response: ${method} ${url} ${response.statusCode} - ${delay}ms`,
                        {
                            error: error.message,
                            stack: error.stack,
                            request: {
                                body: this.sanitizeBody(body),
                                method,
                                url
                            }
                        },
                    );
                }
            })
        );
    }

    private sanitizeHeaders(headers: any): any {
        const sanitized = { ...headers };
        // Remove sensitive information
        delete sanitized.authorization;
        delete sanitized.cookie;
        return sanitized;
    }

    private sanitizeBody(body: any): any {
        if (!body) return 'No body';

        const sanitized = { ...body };
        // Remove sensitive fields if they exist
        if (sanitized.password) delete sanitized.password;
        if (sanitized.token) delete sanitized.token;
        if (sanitized.accessToken) delete sanitized.accessToken;
        if (sanitized.refreshToken) delete sanitized.refreshToken;

        return sanitized;
    }
} 

Now we register the logger at boostrap:

    app.useGlobalInterceptors(new LoggingInterceptor());

More Error Logging for Debug Purpose

By default when an error reach our LoggingInterceptor the error stackTrace is lost. To get more detail on which file and which line the exception occurs at which file, we need the following:

export class AppModule {
    onModuleInit() {
        process.on('uncaughtException', err => {
            console.error('Uncaught Exception:', err.stack);
            console.info('Node NOT Exiting...');
        });

        process.on('unhandledRejection', (reason, promise) => {
            console.error('Unhandled Rejection at:', promise, 'reason:', reason);
        });
    }
}