Careeroza
Response Mapping
A Consistent Success Envelope
Many teams wrap every success response the same way, so the frontend always reads res.data.
src/common/interceptors/transform.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, map } from 'rxjs';
export interface ApiResponse<T> {
success: true;
data: T;
timestamp: string;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
intercept(_context: ExecutionContext, next: CallHandler<T>): Observable<ApiResponse<T>> {
return next.handle().pipe(
map((data) => ({ success: true as const, data, timestamp: new Date().toISOString() })),
);
}
}Example
curl http://localhost:3000/products/7Example output
{"success":true,"data":{"id":7,"name":"boAt Airdopes 141","price":1299,"stock":120,"category":"audio"},"timestamp":"2026-01-15T10:00:00.000Z"}Hiding Fields with ClassSerializerInterceptor
Never send password hashes. Mark fields with @Exclude() from class-transformer and enable the built-in ClassSerializerInterceptor. It only works when you return class instances, not plain objects.
import { Exclude, Expose } from 'class-transformer';
export class UserEntity {
id: number;
email: string;
@Exclude() passwordHash: string;
@Exclude() refreshTokenHash: string | null;
@Expose()
get displayName() { return this.email.split('@')[0]; }
constructor(partial: Partial<UserEntity>) { Object.assign(this, partial); }
}
// main.ts: app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
// controller: return new UserEntity(user);