Nest js : Use I18n service in Exception Filter

4.4k views Asked by At

I have a nestjs-graphql project. I use class-validator and nestjs-i18n module.

I can use i18nService when injected in a service as intended. What I'm struggling to do however is to use i18n in my ExceptionFilter to return translated message from the ValidationPipe handled by class-validator

What I currently have

//app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ItemsModule } from './items/items.module';
import { GraphQLModule } from '@nestjs/graphql';
import { MongooseModule } from '@nestjs/mongoose';
import { ConfigModule } from '@nestjs/config';
import { I18nModule, I18nJsonParser } from 'nestjs-i18n';
import configuration from './config/configuration';
import * as path from 'path';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true, load: [configuration] }),
    MongooseModule.forRoot(process.env.MONGO_URI),
    I18nModule.forRoot({
      fallbackLanguage: 'en',
      parser: I18nJsonParser,
      parserOptions: { path: path.join(__dirname, '/i18n/') },
    }),
    GraphQLModule.forRoot({
      autoSchemaFile: 'schema.gql',
      playground: true,
      introspection: true,
      context: ({ req, connection }) =>
        connection ? { req: connection.context } : { req },
    }),
    ItemsModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}


//main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { AllExceptionsFilter } from './utils/exceptions.filters';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({ transform: true }));
  app.useGlobalFilters(new AllExceptionsFilter(new Logger('Exceptions')));
  app.enableCors();
  const port = process.env.PORT || 3000;
  await app.listen(port);
}
bootstrap();


//AllExceptionFilters.ts

import {
  ExceptionFilter,
  Catch,
  HttpException,
  HttpStatus,
  Logger,
} from '@nestjs/common';

import { ApolloError } from 'apollo-server-errors';
import { MongoError } from 'mongodb';

@Catch(HttpException)
export class AllExceptionsFilter implements ExceptionFilter {
  constructor(private logger: Logger) {}
  async catch(exception: HttpException) {
    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const exceptionMessage = (exception) =>
      exception instanceof MongoError
        ? exception?.message
        : exception?.response?.message;

    this.logger.error(exceptionMessage(exception), exception.stack);
    throw new ApolloError(exceptionMessage(exception), status.toString());
  }
}

My idea was to pass the i18n key to the class to validate :

import { Field, InputType, } from '@nestjs/graphql';
import { Length } from 'class-validator';

@InputType()
export class ItemToValidate {
  @Length(5, 30, { message: 'global.length' }) //i18n Key
  @Field()
  readonly title: string;
 
}


... to use it in AllExceptionsFilter as I would in a service:

@Catch(HttpException)
export class AllExceptionsFilter implements ExceptionFilter {
  constructor(private logger: Logger, private i18n: I18nService) {}
  async catch(exception: HttpException) {
   const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const exceptionMessage = (exception) =>
      exception instanceof MongoError
        ? exception?.message
        : exception?.response?.message;
    const translatedMessage = await this.i18n.translate(
      exceptionMessage(exception),
    );
   ...
  }
}


However, I have a logical error instantiating the Filter class in the boostrap function as I don't know how to access I18nService and inject it from there :


async function bootstrap() {
  const logger = new Logger('bootstrap');
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({ transform: true }));
  app.useGlobalFilters(new AllExceptionsFilter(new Logger('Exceptions'), /*I18nService ?*/ ));

}
bootstrap();

What's the best way to go to achieve this?

2

There are 2 answers

0
BunyamiN On

As shown in the docs you cannot have dependency injection if you register your filter with useGlobalFilters().

Instead you have to do something like this:

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: AllExceptionsFilter,
    },
  ],
})
export class AppModule {}
0
Vladislav On

You can inject services in global filters like this:

.../all.exception-filter.ts:

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
  protected readonly appName = `${process.env.APP_NAME}-${process.env.NODE_ENV}`;

  constructor(
    protected readonly httpAdapterHost: HttpAdapterHost<
      AbstractHttpAdapter<any, any, any>
    >,
    private readonly exceptionLogger: AlarmerService,
  ) {
    super();
  }

  catch(exception: any, host: ArgumentsHost) {
    const contextType = host.getType();

    if (contextType === 'http') {
      return this.httpCatch(exception, host);
    }

    if (RPC_CONTEXTS_CONST.includes(contextType)) {
      return this.rpcCatch(exception, host);
    }

    if (contextType === 'ws') {
      return this.wsCatch(exception, host);
    }

    // super.catch(exception, host);
  }

main.ts:

  const httpAdapter = app.get(HttpAdapterHost);
  const tgAlarmer = app.get(AlarmerService);
  app.useGlobalFilters(
    new AllExceptionsFilter(httpAdapter, tgAlarmer),
    new I18nValidationExceptionFilter(),
  );

Also you have to import services, witch you are trying to app.get() in the main module.