API with NestJS #5. Serializing the response with interceptors

263 阅读2分钟

Sometimes we need to perform additional operations on the outcoming data. We might not want to expose specific properties or modify the response in some other way. In this article, we look into various solutions NestJS provides us with to change the data we send in the response.

Serialization (it's worth to dive into the process of Serialization)

// src/modules/users/entities/user.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Exclude } from 'class-transformer';

@Entity({ name: 'users' })
class UserEntity {
  @PrimaryGeneratedColumn()
  public id?: number;

  @Column({ unique: true })
  public email: string;

  @Column()
  public name: string;

  @Column()
  @Exclude()
  public password: string;
}

export default UserEntity;

NestJS comes equipped with  ClassSerializerInterceptor that uses class-transformer under the hood. To apply the above transformation, we need to use it in our controller:


@Controller('authentication')
@UseInterceptors(ClassSerializerInterceptor)
class AuthenticationController

If we find ourselves adding ClassSerializerInterceptor to a lot of controllers, we can configure it globally instead.

// src/main.ts
import { HttpAdapterHost, NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module/app.module';
import * as cookieParser from 'cookie-parser';
import { ExceptionsLoggerFilter } from './utils/filter/exceptionsLogger.filter';
import { ClassValidationPipe } from './utils/pipe/class-validation.pipe';
import { ClassSerializerInterceptor } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const { httpAdapter } = app.get(HttpAdapterHost);

  app.useGlobalFilters(new ExceptionsLoggerFilter(httpAdapter));
  app.useGlobalPipes(new ClassValidationPipe());

  app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));

  app.use(cookieParser());
  await app.listen(3000, () => {
    console.log('Listening on port 3000');
  });
}
bootstrap();

By default, all properties of our entities are exposed. We can change this strategy by providing additional options to the class-transformer. To do so, we need to use the  @SerializeOptions() decorator.

@Controller('authentication')
@SerializeOptions({
  strategy: 'excludeAll'
})
export class AuthenticationController
// src/modules/users/entities/user.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Expose } from 'class-transformer';

@Entity({ name: 'users' })
class User {
  @PrimaryGeneratedColumn()
  public id?: number;

  @Column({ unique: true })
  @Expose()
  public email: string;

  @Column()
  @Expose()
  public name: string;

  @Column()
  public password: string;
}

export default User;

image.png

image.png

Issues with using the @Res() decorator

Using the  @Res() decorator strips us from some advantages of using NestJS. Unfortunately, it interferes with the  ClassSerializerInterceptor. To prevent that, we can follow some advice from the creator of NestJS. If we use the request.res object instead of the  @Res() decorator, we don’t put NestJS into the express-specific mode.

@HttpCode(200)
@UseGuards(LocalAuthenticationGuard)
@Post('log-in')
async logIn(@Req() request: RequestWithUser) {
  const {user} = request;
  const cookie = this.authenticationService.getCookieWithJwtToken(user.id);
  request.res.setHeader('Set-Cookie', cookie);
  return user;
}

express's typescript support is bad.

Custom interceptors

// src/utils/interceptor/exclude-null.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { recursivelyStripNullValues } from '../helper/recursively-strip-null-values.helper';

@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next
      .handle()
      .pipe(map((value) => recursivelyStripNullValues(value)));
  }
}

Each interceptor needs to implement the  NestInterceptor and, therefore, the  intercept method. It takes two arguments:

  1. ExecutionContext

    • it provides information about the current context,
  2. CallHandler

    • it contains the  handle method that invokes the route handler and returns an RxJS Observable

The  intercept method wraps the request/response stream, and we can add logic both before and after the execution of the route handler. In the above code, we invoke the route handle and modify the response

// src/utils/helper/recursively-strip-null-values.helper.ts
export function recursivelyStripNullValues(value: unknown): unknown {
  if (Array.isArray(value)) {
    return value.map(recursivelyStripNullValues);
  }
  if (value !== null && typeof value === 'object') {
    return Object.fromEntries(
      Object.entries(value).map(([key, value]) => [
        key,
        recursivelyStripNullValues(value),
      ]),
    );
  }
  if (value !== null) {
    return value;
  }
}

Unified response architecture

//
//

will update later.

ref:

source