Dowódco legionów! Wyobraź sobie posłańców Imperium Rzymskiego, którzy muszą przewozić ogromne zwoje pergaminu między prowincjami. Mądry pretorianin kompresuje wiadomości - zamiast wysyłać pełne kroniki, wysyła zwięzłe raporty z najważniejszymi informacjami. W NestJS compression i response optimization to identyczna sztuka - zmniejszamy rozmiar odpowiedzi, aby dotarły szybciej do klienta.
Kompresja to najprostszy sposób na przyspieszenie odpowiedzi HTTP. NestJS wspiera middleware
compression, które automatycznie kompresuje odpowiedzi:1// main.ts - dodanie kompresji
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 // Włącz kompresję gzip/deflate
10 app.use(compression({
11 filter: (req, res) => {
12 // Nie kompresuj odpowiedzi z nagłówkiem x-no-compression
13 if (req.headers['x-no-compression']) {
14 return false;
15 }
16 // Domyślny filtr - kompresuj text/html, application/json, etc.
17 return compression.filter(req, res);
18 },
19 threshold: 1024, // Kompresuj odpowiedzi > 1KB
20 level: 6, // Poziom kompresji (1-9, domyślnie 6)
21 }));
22
23 await app.listen(3000);
24}
25
26bootstrap();class-transformer pozwala kontrolować, które pola obiektu są zwracane w odpowiedzi. To jak cenzor w Imperium, który decyduje jakie informacje mogą opuścić mury Senatu: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 // Ukryj wewnętrzne dane - nie wysyłaj za mury Imperium!
15 @Exclude()
16 internalCode: string;
17
18 @Exclude()
19 secretOrders: string;
20
21 // Transformacja - przelicz na format czytelny dla obywateli
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 // Warunkowe ukrywanie - pokaż tylko jeśli użytkownik ma rangę
31 @Expose({ groups: ['admin', 'senator'] })
32 budget: number;
33}Aby automatycznie stosować serializację, NestJS oferuje
ClassSerializerInterceptor:1// app.module.ts - globalny interceptor serializacji
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 - użycie w kontrolerze
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 // Pola z @Exclude() zostaną automatycznie usunięte z odpowiedzi
27 return this.legionService.findAll();
28 }
29
30 @Get('admin')
31 @SerializeOptions({ groups: ['admin'] })
32 findAllAdmin(): LegionResponseDto[] {
33 // Pola z @Expose({ groups: ['admin'] }) będą widoczne
34 return this.legionService.findAll();
35 }
36}Zamiast wysyłać wszystkie rekordy naraz (jak wysyłanie całej armii jedną drogą), dzielimy odpowiedzi na mniejsze strony:
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 z 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() zwraca POJO zamiast Mongoose Document - szybsze!
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}Nagłówki cache HTTP pozwalają przeglądarkom i CDN-om przechowywać odpowiedzi lokalnie:
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 minut domyślnie
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 - jak długo posłaniec może trzymać kopię
24 response.setHeader(
25 'Cache-Control',
26 `public, max-age=${this.maxAge}, s-maxage=${this.maxAge * 2}`
27 );
28
29 // ETag - identyfikator wersji zasobu
30 // Przeglądarka wysyła If-None-Match i dostaje 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 // Prosty ETag na podstawie timestamp
39 return '"' + Date.now().toString(36) + '"';
40 }
41}
42
43// Użycie w kontrolerze
44@Controller('provinces')
45export class ProvinceController {
46 @Get()
47 @UseInterceptors(new CacheHeadersInterceptor(600)) // 10 minut cache
48 findAll() {
49 return this.provinceService.findAll();
50 }
51
52 @Get(':id')
53 @UseInterceptors(new CacheHeadersInterceptor(3600)) // 1 godzina cache
54 findOne(@Param('id') id: string) {
55 return this.provinceService.findOne(id);
56 }
57}Trzy kluczowe dekoratory z
class-transformer:1// Kompletny przykład
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; // Nigdy nie wysyłaj hasła!
16
17 @Exclude()
18 internalNotes: string; // Wewnętrzne notatki
19
20 @Transform(({ value }) => value > 1000 ? 'Veteranus' : 'Tiro')
21 @Expose()
22 experienceLevel: string; // Transformacja liczbowa -> tekstowa
23
24 @Transform(({ value }) => value?.toISOString())
25 @Expose()
26 enlistedAt: string; // Data w formacie ISO
27}Compression i response optimization to sztuka efektywnej komunikacji w Imperium. Mądry pretor nie wysyła pełnych kronik, gdy wystarczy zwięzły raport. Stosując kompresję, serializację i paginację, Twoja aplikacja będzie odpowiadać szybko i oszczędnie - jak dobrze zorganizowana poczta Imperium Rzymskiego!