Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Compression i Response Optimization - przyspieszenie posłańców Imperium

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.

Compression Middleware - kompresja gzip/deflate

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();

Response Serialization z class-transformer

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}

ClassSerializerInterceptor

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}

Pagination - stronicowanie odpowiedzi

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}

Response Caching Headers

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}

Dekoratory @Exclude, @Expose, @Transform - podsumowanie

Trzy kluczowe dekoratory z

class-transformer
:

  • @Exclude() - ukrywa pole z odpowiedzi (jak tajny rozkaz Cezara)
  • @Expose() - jawnie oznacza pole do wysłania (jak publiczny edykt)
  • @Transform() - modyfikuje wartość przed wysłaniem (jak tłumacz Imperium)
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!

Przejdź do CodeWorlds