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 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();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}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}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}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}Trzy kluczowe dekoratory z
class-transformer: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!