We use cookies to enhance your experience on the site
CodeWorlds

Compression and Response Optimization - speeding up the Empire's messengers

Legion commander! Imagine the messengers of the Roman Empire who must transport huge scrolls of parchment between provinces. A wise praetorian compresses messages - instead of sending full chronicles, he sends concise reports with the most important information. In NestJS, compression and response optimization is the same art - we reduce the response size so they reach the client faster.

Compression Middleware - gzip/deflate compression

Compression is the simplest way to speed up HTTP responses. NestJS supports the

compression
middleware that automatically compresses responses:

1// main.ts - adding compression
2import { NestFactory } from '@nestjs/core';
3import { AppModule } from './app.module';
4import * as compression from 'compression';
5
6async function bootstrap() {
7  const app = await NestFactory.create(AppModule);
8
9  // Enable gzip/deflate compression
10  app.use(compression({
11    filter: (req, res) => {
12      // Don't compress responses with the x-no-compression header
13      if (req.headers['x-no-compression']) {
14        return false;
15      }
16      // Default filter - compress text/html, application/json, etc.
17      return compression.filter(req, res);
18    },
19    threshold: 1024,  // Compress responses > 1KB
20    level: 6,         // Compression level (1-9, default 6)
21  }));
22
23  await app.listen(3000);
24}
25
26bootstrap();

Response Serialization with class-transformer

class-transformer
allows you to control which object fields are returned in the response. It's like a censor in the Empire who decides what information can leave the Senate walls:

1// legion.entity.ts
2import { Exclude, Expose, Transform } from 'class-transformer';
3
4export class LegionResponseDto {
5  @Expose()
6  id: number;
7
8  @Expose()
9  name: string;
10
11  @Expose()
12  province: string;
13
14  // Hide internal data - don't send beyond the Empire's walls!
15  @Exclude()
16  internalCode: string;
17
18  @Exclude()
19  secretOrders: string;
20
21  // Transformation - convert to a citizen-readable format
22  @Transform(({ value }) => Math.round(value))
23  @Expose()
24  soldiers: number;
25
26  @Transform(({ value }) => new Date(value).toLocaleDateString('pl-PL'))
27  @Expose()
28  foundedAt: string;
29
30  // Conditional hiding - show only if the user has the rank
31  @Expose({ groups: ['admin', 'senator'] })
32  budget: number;
33}

ClassSerializerInterceptor

To automatically apply serialization, NestJS offers

ClassSerializerInterceptor
:

1// app.module.ts - global serialization interceptor
2import { Module } from '@nestjs/common';
3import { APP_INTERCEPTOR } from '@nestjs/core';
4import { ClassSerializerInterceptor } from '@nestjs/common';
5
6@Module({
7  providers: [
8    {
9      provide: APP_INTERCEPTOR,
10      useClass: ClassSerializerInterceptor,
11    },
12  ],
13})
14export class AppModule {}
15
16// legion.controller.ts - usage in the controller
17import { Controller, Get, UseInterceptors, SerializeOptions } from '@nestjs/common';
18import { ClassSerializerInterceptor } from '@nestjs/common';
19import { LegionResponseDto } from './legion.entity';
20
21@Controller('legions')
22@UseInterceptors(ClassSerializerInterceptor)
23export class LegionController {
24  @Get()
25  findAll(): LegionResponseDto[] {
26    // Fields with @Exclude() will be automatically removed from the response
27    return this.legionService.findAll();
28  }
29
30  @Get('admin')
31  @SerializeOptions({ groups: ['admin'] })
32  findAllAdmin(): LegionResponseDto[] {
33    // Fields with @Expose({ groups: ['admin'] }) will be visible
34    return this.legionService.findAll();
35  }
36}

Pagination - paginating responses

Instead of sending all records at once (like sending the entire army down one road), we divide responses into smaller pages:

1// pagination.dto.ts
2export class PaginationDto {
3  page?: number = 1;
4  limit?: number = 20;
5  sortBy?: string = 'id';
6  sortOrder?: 'ASC' | 'DESC' = 'ASC';
7}
8
9export class PaginatedResponse<T> {
10  data: T[];
11  meta: {
12    page: number;
13    limit: number;
14    total: number;
15    totalPages: number;
16    hasNextPage: boolean;
17    hasPreviousPage: boolean;
18  };
19}
20
21// legion.service.ts
22import { Injectable } from '@nestjs/common';
23
24@Injectable()
25export class LegionService {
26  async findPaginated(pagination: PaginationDto): Promise<PaginatedResponse<any>> {
27    const { page, limit, sortBy, sortOrder } = pagination;
28    const skip = (page - 1) * limit;
29
30    // MongoDB with Mongoose
31    const [data, total] = await Promise.all([
32      this.legionModel
33        .find()
34        .sort({ [sortBy]: sortOrder === 'ASC' ? 1 : -1 })
35        .skip(skip)
36        .limit(limit)
37        .lean()      // lean() returns POJO instead of Mongoose Document - faster!
38        .exec(),
39      this.legionModel.countDocuments(),
40    ]);
41
42    const totalPages = Math.ceil(total / limit);
43
44    return {
45      data,
46      meta: {
47        page,
48        limit,
49        total,
50        totalPages,
51        hasNextPage: page < totalPages,
52        hasPreviousPage: page > 1,
53      },
54    };
55  }
56}

Response Caching Headers

HTTP cache headers allow browsers and CDNs to store responses locally:

1// cache-headers.interceptor.ts
2import {
3  Injectable,
4  NestInterceptor,
5  ExecutionContext,
6  CallHandler,
7} from '@nestjs/common';
8import { Observable } from 'rxjs';
9import { tap } from 'rxjs/operators';
10import { Response } from 'express';
11
12@Injectable()
13export class CacheHeadersInterceptor implements NestInterceptor {
14  constructor(
15    private readonly maxAge: number = 300, // 5 minutes by default
16  ) {}
17
18  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
19    const response = context.switchToHttp().getResponse<Response>();
20
21    return next.handle().pipe(
22      tap(() => {
23        // Cache-Control - how long the messenger can keep a copy
24        response.setHeader(
25          'Cache-Control',
26          `public, max-age=${this.maxAge}, s-maxage=${this.maxAge * 2}`
27        );
28
29        // ETag - resource version identifier
30        // Browser sends If-None-Match and gets 304 Not Modified
31        const etag = this.generateETag(response);
32        response.setHeader('ETag', etag);
33      }),
34    );
35  }
36
37  private generateETag(response: Response): string {
38    // Simple ETag based on timestamp
39    return '"' + Date.now().toString(36) + '"';
40  }
41}
42
43// Usage in the controller
44@Controller('provinces')
45export class ProvinceController {
46  @Get()
47  @UseInterceptors(new CacheHeadersInterceptor(600)) // 10 minutes cache
48  findAll() {
49    return this.provinceService.findAll();
50  }
51
52  @Get(':id')
53  @UseInterceptors(new CacheHeadersInterceptor(3600)) // 1 hour cache
54  findOne(@Param('id') id: string) {
55    return this.provinceService.findOne(id);
56  }
57}

@Exclude, @Expose, @Transform decorators - summary

Trzy kluczowe dekoratory z

class-transformer
:

  • @Exclude() - hides a field from the response (like Caesar's secret order)
  • @Expose() - explicitly marks a field for sending (like a public edict)
  • @Transform() - modifies the value before sending (like the Empire's translator)
1// Complete example
2import { Exclude, Expose, Transform, Type } from 'class-transformer';
3
4export class SoldierResponse {
5  @Expose()
6  id: number;
7
8  @Expose()
9  name: string;
10
11  @Expose()
12  rank: string;
13
14  @Exclude()
15  passwordHash: string;     // Never send the password!
16
17  @Exclude()
18  internalNotes: string;    // Internal notes
19
20  @Transform(({ value }) => value > 1000 ? 'Veteranus' : 'Tiro')
21  @Expose()
22  experienceLevel: string;  // Numeric -> text transformation
23
24  @Transform(({ value }) => value?.toISOString())
25  @Expose()
26  enlistedAt: string;       // Date in ISO format
27}

Compression and response optimization is the art of effective communication in the Empire. A wise praetor does not send full chronicles when a concise report will suffice. By applying compression, serialization, and pagination, your application will respond quickly and efficiently - like a well-organized Roman Empire postal service!

Go to CodeWorlds