typescript 91 lines · 3 tabs

Hide Sensitive Fields in NestJS Responses with class-transformer and ClassSerializerInterceptor

Shared by codesnips Aug 2026
3 tabs
import { Exclude, Expose } from 'class-transformer';

export class User {
  id: string;

  firstName: string;

  lastName: string;

  @Expose({ groups: ['admin'] })
  email: string;

  @Exclude()
  password: string;

  @Exclude()
  twoFactorSecret: string | null;

  createdAt: Date;

  constructor(partial: Partial<User>) {
    Object.assign(this, partial);
  }

  @Expose()
  get fullName(): string {
    return `${this.firstName} ${this.lastName}`.trim();
  }
}
3 files · typescript Explain with highlit

This snippet shows how NestJS serializes outgoing responses through class-transformer, using decorators to strip sensitive data before it leaves the controller. The core idea is that a controller should return a class instance (an entity), and a ClassSerializerInterceptor transforms that instance into a plain object right before the response is written, honoring @Exclude() and @Expose() metadata along the way. This keeps field-hiding declarative and centralized instead of scattering delete user.password calls across handlers.

In user.entity.ts, the User class annotates persistence-only fields like password and twoFactorSecret with @Exclude(), so they never appear in serialized output regardless of which endpoint returns the object. email is wrapped with @Expose({ groups: ['admin'] }), meaning it is only emitted when the serialization context requests the admin group — a common pattern for showing more data to privileged callers. The fullName getter uses @Expose() to add a computed field that has no database column, demonstrating that serialization operates on the runtime instance, not the schema.

Because class-transformer only excludes fields it knows about, the entity constructor uses Object.assign so plain rows from the ORM become real User instances that carry the decorator metadata. Returning a bare object would bypass the interceptor entirely, which is the most common pitfall.

In users.controller.ts, @UseInterceptors(ClassSerializerInterceptor) activates serialization for the controller, and @SerializeOptions({ groups: ['admin'] }) on findForAdmin passes the group into the transform so admin routes see email while the public findOne route does not. The @Get() handlers simply return User instances and let the interceptor do the hiding.

In main.ts, app.useGlobalInterceptors wires the interceptor app-wide using the DI Reflector, and the global ValidationPipe with transform: true and whitelist: true ensures inbound DTOs are also class instances with unknown properties dropped. A key trade-off: excludeExtraneousValues is deliberately not enabled here, so only decorated fields are hidden; teams wanting an allow-list model would flip that on and @Expose() everything intended to ship. This approach is worth reaching for whenever the same entity is returned by many endpoints and consistent, auditable field-hiding matters more than per-handler control.


Related snips

Share this code

Here's the card — post it anywhere.

Hide Sensitive Fields in NestJS Responses with class-transformer and ClassSerializerInterceptor — share card
Link copied