Implementarea Aplicațiilor Event-Driven Utilizând Nest.js
In era în care tehnologia informației primează, dezvoltarea aplicațiilor web complexe și scalabile devine una dinamizată de o necesitate imperativă. În fața acestui nou paradigme digitale, comunitatea programatorilor adoptă, cu consacrație, arhitectura eveniment-driven, considerată fundamentul pentru crearea de aplicații agile și reactive. Prezentul articol ilustrează Nest.js - un framework de programare de notorietate, bazat pe TypeScript, ce înfăptuiește paradigma eveniment-driven - și analizează cu exactitudine beneficiile și abordările practice în generarea aplicațiilor reactive prin intermediul acestei soluții ingenioase.
Introducere
Arhitectura eveniment-driven, aflată la fundamentul dezvoltării aplicațiilor, se caracterizează prin comunicație asincronă între componentele software, conferind astfel o reactivitate sporită și o scalabilitate superioară. Prin intermediul mecanismelor de definire a evenimentelor și acțiunilor specifice, aplicația dobândește fluidețe în funcționare, în timp ce separarea evidentă a responsabilităților facilitează dezvoltarea modulară și menținerea sistematică.
Nest.js se remarcă ca un cadru de programare contemporan, consacrat platformei Node.js și permeat de TypeScript. Această alianță sinergistică aduce în prim-plan un nivel sporit de tipizare statică, eradicând, astfel, erorile în timpul dezvoltării. În mod esențial, Nest.js se conformează paradigmelor eveniment-driven, permițând dezvoltarea aplicațiilor reactive, cu un grad avansat de extensibilitate și examinare concisă.
Erigarea aplicațiilor reactive cu Nest.js presupune adoptarea cu o minuțioasă atenție a Decoraților și Observabililor. Prin intermediul Decoraților, se adnotază clasele și metodele, conturând evenimentele, ruterele, modulele și alte elemente esențiale în arhitectura eveniment-driven. Observabilii, surprenante fluxuri de date asincrone, facilitează tratarea evenimentelor și procesarea reactivă a informațiilor.
Unul dintre atuurile programării reactive cu Nest.js rezidă în ingeniosul său abordare a modularității. Cadru de lucru încurajează dezvoltarea modulară a aplicațiilor, contribuind la izolarea funcționalităților, reutilizarea codului și implementarea unei structuri organizatorice. În implementarea de module independente, echipa de dezvoltare beneficiază de simultaneitate, crescând, astfel, productivitatea și mărirea calitativă a proiectului.
În universul aplicațiilor event-driven cu Nest.js, se manifestă strălucitoarele fluxuri reactive de date. Prin intermediul operatorilor Observabililor, programatorii definesc fluxuri de date complexe, exercitând operațiuni precum map, filter și merge. Această abordare declarativă amplifică limpezimea codului și contribuie la gestionarea logică a evenimentelor, conferind aplicației un farmec sublim.
Avantajele framework-ului
- Inserția de dependențe (Dependency injection)
- Integrare abstractă cu bazele de date
- Soluții abstractizate pentru cazuri de utilizare comune: memorare cache, configurare, versiuni și documentație API, planificarea sarcinilor, cozi, jurnale, module cookie, evenimente și sesiuni, validare a cererilor, server HTTP (Express sau Fastify), autentificare (auth).
- TypeScript (și decoratori)
- Alte elemente de design pentru aplicații excepționale: intermediari (middleware), filtre de excepție, gardieni (guards), conducte (pipes) și altele.
- Și încă mai mult, despre care voi vorbi ulterior.
Unul dintre principalele avantaje ale utilizării unui cadru este posibilitatea de a beneficia de inserția de dependențe, eliminând suprasolicitarea în crearea și susținerea unui arbore de dependențe al claselor. Integrează în mod abstract majoritatea bazelor de date, astfel încât să nu fie necesară o prea mare preocupare în această privință.
Unele dintre cele mai dezvoltate și populare pachete suportate sunt mongoose, TypeORM, MikroORM și Prisma. Acesta furnizează soluții abstractizate pentru cazurile de utilizare comune în dezvoltarea web, cum ar fi memorarea cache-ului, configurarea, versiunile și documentația API, cozi etc.
În ceea ce privește serverul HTTP, puteți alege între Express sau Fastify. Se folosește TypeScript și decoratori, fapt ce simplifică citirea codului, mai ales în proiecte mai ample, și permite ca echipa de dezvoltatori să fie pe aceeași lungime de undă în ceea ce privește argumentarea componentelor. De asemenea, ca orice cadru, acesta oferă și alte elemente de design pentru aplicații, cum ar fi intermediari, filtre de excepție, gardieni, conducte etc.
Scalabilitate
Într-un curs de explorare a arhitecturilor software, Nest.js se dovedește a fi un cadru (framework) bogat în opțiuni pentru construcția aplicațiilor cu o scalabilitate excepțională. Vom examina cu atenție variantele disponibile, precum și modul în care fiecare abordare subliniază esența unei dezvoltări solide și eficiente:
Monolit (modular)
În această paradigmă, Nest.js permite construirea unei aplicații monolitice, în care componente strâns interconectate coexistă într-o unitate singulară. Acestemono elemente, datorită cuplării lor stricte, sunt implementate și susținute împreună, formând astfel un ansamblu indivizibil. Pentru a remedia potențialele provocări apărute în dezvoltarea unei aplicații monolitice de amploare, cadru Nest.js încurajează o abordare modulară, prin care entitățile sistemului se manifestă ca entități independente într-o oarecare măsură, oferind astfel posibilitatea ca echipe diferite să lucreze asupra acestora. Această abordare reprezintă o soluție pragmatică, cu atât mai mult cu cât proiectul se dezvoltă și se lărgește în complexitate.
Microservicii
Nest.js se revelează a fi de o deosebită utilitate în contextul utilizării microserviciilor. Această abordare arhitecturală constă în adoptarea unor deploimente separate pentru fiecare serviciu individual. În mod obișnuit, fiecare microserviciu are sarcina de a gestiona o unitate redusă de muncă și dispune de propriul său stoc de date. Deosebit de interesant, acest principiu de design coincide într-o mare măsură cu abordarea bazată pe evenimente, generând astfel sinergii și convergențe notabile între cele două concepte. Comunicarea dintre servicii se realizează într-un mod elegant și abstract, unde fiecare serviciu emite evenimente către restul entităților, fără a mai monitoriza în detaliu parcurgerea fluxurilor de date.
Abordarea bazată pe evenimente
Subliniind complexitatea abordării bazate pe evenimente, Nest.js înfățișează o arhitectură ce se distinge printr-o comunicare indirectă între servicii. În cadrul acestei paradigme, fiecare serviciu emite evenimente în mod autonom, neinteresându-se ulterior de soarta acestora. Un aspect notabil îl reprezintă prezența ascultătorilor evenimentelor, însă aceștia nu sunt obligatorii, conferind astfel o libertate sporită în dezvoltarea componentelor. Dacă un eveniment este consumat de către o altă entitate, acesta poate genera un alt eveniment, perpetuând astfel fluxul de comunicare. În final, un serviciu va produce un răspuns pentru clientul care așteaptă, adoptând diverse tehnici de comunicare, precum răspunsuri WebSocket sau webhook-uri.
Arhitectură mixtă
Pentru a face față complexității unor proiecte ample și în evoluție, adesea opțiunile de arhitectură Nest.js se combină pentru a forma o abordare mixtă. Proiectele mari sunt, în esență, o amalgamare a diferitelor modele de design - unele componente pot fi puternic cuplate și implementate împreună, în timp ce altele se dezvoltă și se implementează separat, iar altele interacționează în mod exclusiv prin intermediul comunicării bazate pe evenimente. Această abordare complexă se dovedește adesea a fi optimă pentru a satisface nevoile variate și sofisticate ale unor proiecte robuste și scalabile.
Dezvoltarea de aplicații bazate pe evenimente
Nest.js integrează cu ușurință pachetul Bull pentru gestionarea cozilor (github.com/OptimalBits/bull), permițând o integrare rapidă și simplă a acestuia în proiectele noastre. Gestionarea cozilor este deosebit de benefică pentru dezvoltarea de aplicații distribuite, unde sarcinile pot fi gestionate în mod asincron, optimizând astfel performanța și reactivitatea aplicației.
Pentru dezvoltarea și comunicarea între microservicii, Nest.js oferă integrare cu cele mai populare platforme de mesagerie, precum Redis, Kafka, RabbitMQ, MQTT, NATS și altele. Această integrare facilitează schimbul eficient de date între diverse componente ale aplicației, contribuind la construirea unor aplicații scalabile și modularizate.
Nest.js promovează dezvoltarea modulară, permițând componentelor aplicației să fie extrase în unități de lucru independente, pe măsură ce proiectul avansează în ciclul de viață. Această abordare organizată asigură o structură coerentă și clară, facilitând dezvoltarea rapidă și flexibilă a funcționalităților.
Un alt aspect notabil al Nest.js este documentația sa completă și exemplele exemplare. Aceasta oferă un ghid valoros dezvoltatorilor, oferindu-le resurse esențiale pentru a înțelege și implementa cu succes caracteristicile oferite de cadru, accelerând astfel procesul de dezvoltare și asigurând o învățare facilă și rapidă.
De asemenea, Nest.js facilitează testarea unitară și de integrare prin suportul nativ pentru Dependency Injection și framework-ul Jest. Acest lucru permite dezvoltatorilor să creeze teste robuste și fiabile, identificând și corectând erorile sau defecțiunile cu ușurință și precizie.
În continuare, prezentăm codul pentru crearea unei cozi simple în NestJS:
// Instalarea dependențelor necesare
npm install --save @nestjs/bull bull
npm install --save-dev @types/bull
// Crearea conexiunii cu Redis
import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common';
@Module({
imports: [
BullModule.forRootAsync({
useFactory: () => ({
redis: {
host: 'localhost',
port: 6379,
},
}),
}),
],
})
export class AppModule {}
Integrarea Mesajelor — Conexiunea
Începutul conexiunii furnizorului de mesaje presupune adăugarea unei conexiuni la modulul client. În acest exemplu, utilizăm transportul Redis și trebuie să furnizăm opțiunile specifice pentru conexiunea Redis. Prezentăm un exemplu de înregistrare a modulului client pentru mesagerie în Nest.js:
// Import the necessary modules
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
@Module({
imports: [
ClientsModule.register([
{
name: 'MATH_SERVICE',
transport: Transport.REDIS,
options: {
host: 'localhost',
port: 6379,
},
}),
],
})
export class AppModule {}
În acest fragment de cod, observăm utilizarea modulului ClientsModule pentru a înregistra un nou client pentru mesagerie. În exemplul nostru, numim clientul "MATH_SERVICE". Acesta folosește transportul Redis, iar opțiunile pentru conexiune sunt specificate pentru adresa "localhost" și portul "6379".
Acesta este doar un exemplu simplu de înregistrare a unui client pentru mesagerie în Nest.js. Prin intermediul acestui client, aplicația poate comunica cu alte servicii sau microservicii, fie în cadrul aceleiași aplicații, fie în aplicații separate, utilizând sistemul de mesagerie specificat, în acest caz Redis.
Prin intermediul unor astfel de conexiuni și a funcționalităților oferite de Nest.js, dezvoltatorii pot crea aplicații distribuite și scalabile, care comunică între ele prin intermediul mesajelor, oferind astfel o arhitectură robustă și flexibilă pentru nevoile complexe ale proiectelor moderne.
Integrarea Mesajelor — Producătorul
Următorul pas este să injectăm interfața de proxy a clientului în serviciul nostru producător. Prezentăm un exemplu de injectare a modulului client pentru mesagerie într-o clasă de serviciu în Nest.js:
import { Inject, Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
@Injectable()
export class MathService {
constructor(@Inject('MATH_SERVICE') private client: ClientProxy) {}
}
În exemplul de mai sus, observăm utilizarea decoratorului @Inject pentru a injecta clientul de mesagerie în serviciul nostru MathService. Acesta va utiliza clientul cu numele "MATH_SERVICE" pe care l-am înregistrat anterior în modulul nostru.
Următorul pas este să decidem între utilizarea metodei SEND sau a metodei EMIT. Metoda SEND reprezintă de obicei o acțiune sincronă, asemănătoare unei cereri HTTP, dar este abstractizată de către framework pentru a acționa prin transportul selectat. În exemplul de mai jos, metoda accumulate() nu va trimite răspunsul către client până când mesajul nu este procesat de aplicația ascultătorului.
import { Observable } from 'rxjs';
accumulate(): Observable<number> {
const pattern = { cmd: 'sum' };
const payload = [1, 2, 3];
return this.client.send<number>(pattern, payload);
}
Pe de altă parte, comanda EMIT reprezintă un început de flux de lucru asincron, acționând fie ca o notificare "fire and forget", fie ca un eveniment de coadă durabilă în unele sisteme de transport. Aceasta depinde de transportul ales și de configurarea sa. Un exemplu de emitere a unui mesaj în Nest.js către un serviciu extern prin intermediul broker-ului de mesaje este prezentat mai jos:
import { UserCreatedEvent } from './user-created.event';
async publish() {
this.client.emit<number>('user_created', new UserCreatedEvent());
}
În concluzie, modul de utilizare al metodelor SEND și EMIT poate varia în funcție de specificul aplicației și de transportul mesajelor ales. Nest.js oferă suport flexibil pentru aceste metode, permițând dezvoltatorilor să implementeze integrări eficiente și sincronizate cu sistemul de mesagerie, oferind astfel o modalitate elegantă și performantă de a comunica între diferite servicii și componente ale aplicației.
Integrarea Mesajelor — Consumatorul
Decoratorul MessagePattern este destinat exclusiv metodelor asemănătoare sincronizării (create cu comanda SEND) și poate fi utilizat numai în cadrul unei clase decorate cu @Controller. Astfel, se așteaptă un răspuns la cererea primită prin protocolul nostru de mesagerie. Prezentăm un exemplu de răspuns din Nest.js către un serviciu extern prin intermediul broker-ului de mesaje:
import { Controller, MessagePattern } from '@nestjs/common';
@Controller()
export class MathController {
@MessagePattern({ cmd: 'sum' })
accumulate(data: number[]): number {
return (data || []).reduce((a, b) => a + b);
}
}
Pe de altă parte, decoratorul EventPattern poate fi utilizat în orice clasă personalizată a aplicației și va asculta evenimentele produse pe aceeași coadă sau bus de evenimente, fără a se aștepta să se returneze ceva din aplicație.
Prezentăm un exemplu de procesare a unui mesaj dintr-un serviciu extern în Nest.js prin intermediul broker-ului de mesaje:
import { EventPattern } from '@nestjs/microservices';
@EventPattern('user_created')
async handleUserCreated(data: Record<string, unknown>) {
// logic here
}
Această configurare este similară cu alte sisteme de mesagerie. Dacă utilizăm un sistem personalizat, putem totuși folosi un container DI și putem crea un furnizor personalizat de sistem de evenimente cu ajutorul interfețelor oferite de Nest.js.
Exemple de consumatori pentru MQTT și NATS în Nest.js:
@MessagePattern('notifications')
getNotifications(@Payload() data: number[], @Ctx() context: MqttContext) {
console.log(`Topic: ${context.getTopic()}`);
}
// NATS
@MessagePattern('notifications')
getNotifications(@Payload() data: number[], @Ctx() context: NatsContext) {
console.log(`Subject: ${context.getSubject()}`);
}
Exemple de consumatori pentru RabbitMQ și Kafka în Nest.js:
@MessagePattern('notifications')
getNotifications(@Payload() data: number[], @Ctx() context: RmqContext) {
console.log(`Pattern: ${context.getPattern()}`);
}
// Kafka
@MessagePattern('hero.kill.dragon')
killDragon(@Payload() message: KillDragonMessage, @Ctx() context: KafkaContext) {
console.log(`Topic: ${context.getTopic()}`);
}
Aceasta demonstrează cât de ușor este să integram aplicația noastră cu cele mai comune sisteme de mesagerie folosind abstractiile oferite de Nest.js.