Zaawansowane Koncepcje w NestJS

NestJS oferuje wiele zaawansowanych narzędzi i funkcjonalności, które pozwalają na tworzenie skalowalnych i nowoczesnych aplikacji serwerowych. W tym wpisie na blogu omówimy trzy zaawansowane koncepcje w NestJS: interceptorzy, komunikacja w czasie rzeczywistym z WebSockets oraz asynchroniczne taski i harmonogramowanie. Dzięki temu dowiesz się, jak wzbogacić swoją aplikację o nowe możliwości, które mogą znacznie poprawić wydajność, użyteczność i nowoczesność aplikacji.

Interceptorzy w NestJS

Czym są interceptorzy?

Interceptorzy są specjalnym rodzajem middleware, który działa w trakcie przetwarzania zapytania przez serwer. W odróżnieniu od typowego middleware, interceptorzy mają dostęp zarówno do danych przed przetworzeniem przez kontroler, jak i po ich przetworzeniu, co pozwala m.in. na modyfikowanie odpowiedzi, logowanie i obsługę czasu przetwarzania. Ich użycie można porównać do tzw. „haków”, które pozwalają wchodzić w różne etapy przetwarzania żądania/odpowiedzi.

Tworzenie własnego interceptora

Poniżej przedstawiamy przykładowy interceptor, który loguje czas przetwarzania zapytań:

				
					import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LogTimeInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const start = Date.now();
    return next
      .handle()
      .pipe(
        tap(() =>
          console.log(
            `Request took ${Date.now() - start}ms`,
          ),
        ),
      );
  }
}

				
			

Interceptor LogTimeInterceptor mierzy czas trwania żądania. ExecutionContext pozwala uzyskać szczegóły dotyczące aktualnego kontekstu, a CallHandler umożliwia przekazywanie kontroli do kolejnych interceptorów lub docelowego kontrolera.

Rejestracja interceptora

Aby zarejestrować interceptor, możemy go użyć w kontrolerze lub na poziomie całej aplikacji. Przykład na poziomie kontrolera:

				
					import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { LogTimeInterceptor } from './log-time.interceptor';

@Controller('uzytkownicy')
@UseInterceptors(LogTimeInterceptor)
export class UzytkownikController {
  @Get()
  znajdzWszystkich() {
    return [
      { imie: 'Jan', nazwisko: 'Kowalski' },
      { imie: 'Anna', nazwisko: 'Nowak' },
    ];
  }
}

				
			

W ten sposób LogTimeInterceptor zostanie wywołany za każdym razem, gdy ktoś wywoła endpoint GET /uzytkownicy.

Modyfikacja odpowiedzi

Interceptorzy mogą być także używani do modyfikacji odpowiedzi. Poniżej przedstawiamy przykład, który dodaje dodatkową informację do odpowiedzi:

				
					import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class TransformResponseInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map((data) => ({
        status: 'success',
        data,
      })),
    );
  }
}

				
			

Powyższy interceptor opakowuje każdą odpowiedź w obiekt zawierający pole status z wartością success oraz pole data zawierające właściwą odpowiedź.

Eventy i WebSockets

Komunikacja w czasie rzeczywistym z WebSockets

NestJS posiada doskonałe wsparcie dla komunikacji w czasie rzeczywistym, oferując moduł WebSocket Gateway. Dzięki WebSocket możemy przesyłać dane między serwerem a klientem natychmiast po ich zmianie, co pozwala na tworzenie dynamicznych aplikacji (np. chaty, gry, monitorowanie na żywo).

Instalacja WebSocket Gateway

Aby korzystać z WebSocket Gateway w NestJS, najpierw musimy zainstalować odpowiednie zależności:

				
					npm install @nestjs/websockets @nestjs/platform-socket.io

				
			

Tworzenie prostego WebSocket Gateway

Stwórzmy prosty WebSocket Gateway, który pozwoli nam na emitowanie i nasłuchiwanie wiadomości.

				
					import { WebSocketGateway, SubscribeMessage, MessageBody, WebSocketServer } from '@nestjs/websockets';
import { Server } from 'socket.io';

@WebSocketGateway()
export class ChatGateway {
  @WebSocketServer()
  server: Server;

  @SubscribeMessage('sendMessage')
  handleMessage(@MessageBody() message: string): void {
    console.log(`Received message: ${message}`);
    this.server.emit('newMessage', message);
  }
}

				
			
  • @WebSocketGateway() – Dekorator oznaczający klasę jako WebSocket Gateway.
  • @WebSocketServer() – Umożliwia dostęp do serwera WebSocket (Server z socket.io).
  • @SubscribeMessage('sendMessage') – Dekorator definiujący, jak obsługiwać wiadomości od klientów.

W powyższym przykładzie metoda handleMessage nasłuchuje na wiadomości sendMessage od klienta i emituje nową wiadomość newMessage do wszystkich klientów.

Klient nasłuchujący

Klient (np. przeglądarka) może łączyć się z naszym WebSocket Gateway za pomocą biblioteki socket.io-client. Przykład (JavaScript):

				
					import { io } from "socket.io-client";

const socket = io('http://localhost:3000');

socket.on('newMessage', (message) => {
  console.log(`Received new message: ${message}`);
});

socket.emit('sendMessage', 'Hello, WebSocket!');

				
			

Dzięki temu komunikacja między serwerem a klientem odbywa się w czasie rzeczywistym.

Taski asynchroniczne i harmonogramowanie

Harmonogramowanie zadań cyklicznych

Często w aplikacjach serwerowych musimy wykonywać pewne zadania cyklicznie, np. sprawdzanie stanu aplikacji, synchronizacja danych, wysyłanie powiadomień, itp. NestJS udostępnia narzędzie @nestjs/schedule, które pozwala na harmonogramowanie zadań.

Instalacja NestJS Schedule

Najpierw zainstalujmy zależności:

				
					npm install @nestjs/schedule
npm install --save-dev @types/cron

				
			

Konfiguracja zadania cyklicznego

Po zainstalowaniu @nestjs/schedule możemy stworzyć zadanie cykliczne. Poniższy przykład pokazuje, jak skonfigurować zadanie wykonywane co minutę:

				
					import { Injectable } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';

@Injectable()
export class TasksService {
  @Cron('*/1 * * * *')
  handleCron() {
    console.log('Running a task every minute');
  }
}

				
			
  • @Cron('*/1 * * * *') – Używamy notacji CRON, która mówi, że zadanie ma być uruchamiane co minutę.

Aby nasz serwis działał, musimy zarejestrować ScheduleModule w module aplikacji (AppModule):

				
					import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
import { TasksService } from './tasks.service';

@Module({
  imports: [ScheduleModule.forRoot()],
  providers: [TasksService],
})
export class AppModule {}

				
			

Taski asynchroniczne

Często chcemy także, aby nasze taski działały asynchronicznie, co pozwala na wykonywanie bardziej skomplikowanych operacji, które nie blokują reszty aplikacji:

				
					import { Injectable } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';

@Injectable()
export class TasksService {
  @Cron('*/5 * * * *')
  async handleAsyncCron() {
    console.log('Running an async task every 5 minutes...');
    await new Promise((resolve) => setTimeout(resolve, 3000));
    console.log('Finished async task');
  }
}

				
			

W powyższym przykładzie zadanie jest wykonywane co 5 minut, a wewnątrz metody korzystamy z await, aby poczekać na zakończenie jakiejś asynchronicznej operacji.

Podsumowanie

W tym wpisie poznaliśmy trzy zaawansowane koncepcje w NestJS:

  1. Interceptorzy – Narzędzie do logowania, modyfikacji odpowiedzi oraz modyfikowania przebiegu przetwarzania żądania i odpowiedzi. Dzięki interceptorom możemy śledzić czas wykonania operacji oraz dostosować odpowiedzi w jednolity sposób.

  2. WebSockets i Eventy – Mechanizm do komunikacji w czasie rzeczywistym. Dzięki WebSocket Gateway możemy tworzyć aplikacje, które natychmiast reagują na zmiany, takie jak czaty czy transmisje na żywo.

  3. Taski asynchroniczne i harmonogramowanie – Harmonogramowanie zadań za pomocą @nestjs/schedule, co pozwala na uruchamianie okresowych procesów, takich jak synchronizacja danych, wysyłanie powiadomień czy inne rutynowe czynności.

Każda z tych koncepcji daje nam ogromne możliwości w zakresie rozbudowy funkcjonalności aplikacji. Dzięki zaawansowanym funkcjom NestJS możemy tworzyć aplikacje, które są nie tylko skalowalne, ale także wydajne, dynamiczne i bezpieczne. Zachęcam do eksperymentowania z przedstawionymi narzędziami, ponieważ ich opanowanie pozwala na znaczący rozwój jako programista NestJS.