Mikroserwisy w NestJS w 2026: architektura, gRPC i pytania rekrutacyjne

Praktyczny przewodnik po architekturze mikroserwisów NestJS z gRPC: granice serwisów, warstwy transportowe, wzorce strumieniowania oraz najczęstsze pytania rekrutacyjne na 2026 rok.

Architektura mikroserwisów NestJS z komunikacją gRPC między rozproszonymi serwisami backendowymi

Mikroserwisy NestJS stały się standardowym podejściem do budowy rozproszonych backendów Node.js w 2026 roku. Dzięki ulepszonej warstwie transporterów w NestJS 11, natywnej obsłudze gRPC oraz automatycznej propagacji trace ID framework dostarcza wszystko, czego potrzeba do rozbicia monolitu na dobrze odgraniczone serwisy bez utraty komfortu pracy programisty.

gRPC kontra REST w komunikacji wewnętrznej

gRPC korzysta z Protocol Buffers oraz HTTP/2, oferując nawet 10-krotnie szybszą serializację i natywne strumieniowanie dwukierunkowe w porównaniu z JSON przez REST. W komunikacji serwis-serwis wewnątrz klastra gRPC zmniejsza opóźnienia i wymusza ścisłe kontrakty poprzez pliki .proto. REST pozostaje lepszym wyborem dla publicznych API obsługiwanych przez przeglądarki i podmioty zewnętrzne.

Architektura warstwy transportowej mikroserwisów NestJS

Pakiet @nestjs/microservices zastępuje kontrolery HTTP handlerami zorientowanymi na wiadomości. Zamiast routingu po ścieżce URL każdy handler reaguje na wzorzec wiadomości (message pattern) — ciąg znaków lub klucz obiektu, który identyfikuje operację. Ta abstrakcja sprawia, że ta sama logika handlera działa na TCP, Redisie, NATS, Kafce, RabbitMQ czy gRPC bez zmian w kodzie.

NestJS 11 wprowadził metodę unwrap() na wszystkich transporterach, dającą bezpośredni dostęp do bazowej instancji klienta. Rozwiązuje to długotrwały problem: inspekcję stanu połączenia, strojenie interwałów keepalive czy dostęp do funkcji specyficznych dla brokera, których abstrakcja NestJS nie udostępnia.

Dwa wzorce komunikacji napędzają mikroserwisy NestJS:

  • Żądanie-odpowiedź (@MessagePattern): klient wysyła wiadomość i czeka na odpowiedź. Odpowiednie dla zapytań synchronicznych, takich jak pobranie profilu użytkownika czy weryfikacja tokenu.
  • Zdarzeniowy (@EventPattern): klient emituje zdarzenie i kontynuuje pracę — nie oczekuje odpowiedzi. Idealne dla logów audytowych, powiadomień czy wyzwalania procesów po stronie odbiorcy.
orders.controller.tstypescript
import { Controller } from '@nestjs/common';
import { MessagePattern, EventPattern, Payload } from '@nestjs/microservices';
import { OrdersService } from './orders.service';
import { CreateOrderDto } from './dto/create-order.dto';

@Controller()
export class OrdersController {
  constructor(private readonly ordersService: OrdersService) {}

  // Request-response: caller waits for the created order
  @MessagePattern('order.create')
  async createOrder(@Payload() data: CreateOrderDto) {
    return this.ordersService.create(data);
  }

  // Event-based: fire and forget, no response returned
  @EventPattern('order.shipped')
  async handleOrderShipped(@Payload() data: { orderId: string }) {
    await this.ordersService.markAsShipped(data.orderId);
  }
}

Handler @MessagePattern automatycznie serializuje wartość zwracaną i odsyła ją przez transport. Handler @EventPattern nie zwraca niczego — NestJS odrzuca każdą wartość zwracaną.

Konfiguracja gRPC jako transportu mikroserwisu NestJS

Integracja gRPC w NestJS zaczyna się od pliku .proto, który definiuje kontrakt serwisu. Protocol Buffers wymuszają bezpieczeństwo typów na poziomie sieci — zarówno klient, jak i serwer muszą zgodzić się na dokładny kształt każdej wiadomości, zanim choćby jeden bajt przejdzie przez sieć.

proto/users.protoprotobuf
syntax = "proto3";

package users;

service UsersService {
  rpc FindOne (UserById) returns (User);
  rpc FindMany (UserFilter) returns (stream User);
}

message UserById {
  string id = 1;
}

message UserFilter {
  string role = 1;
  int32 limit = 2;
}

message User {
  string id = 1;
  string email = 2;
  string name = 3;
  string role = 4;
}

Po stronie serwera aplikacja NestJS uruchamia mikroserwis gRPC, wskazując plik proto i wiążąc adres sieciowy:

main.tstypescript
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { join } from 'path';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    AppModule,
    {
      transport: Transport.GRPC,
      options: {
        package: 'users',
        protoPath: join(__dirname, 'proto/users.proto'),
        url: '0.0.0.0:5000',
      },
    },
  );
  await app.listen();
}
bootstrap();

Kontroler używa @GrpcMethod zamiast @MessagePattern. Dekorator przyjmuje nazwę serwisu i nazwę metody RPC jako argumenty, mapując je bezpośrednio na definicję proto:

users.controller.tstypescript
import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
import { UsersService } from './users.service';

@Controller()
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @GrpcMethod('UsersService', 'FindOne')
  async findOne(data: { id: string }) {
    return this.usersService.findById(data.id);
  }
}

NestJS automatycznie obsługuje ładowanie proto, serializację i zarządzanie połączeniem HTTP/2. Metoda kontrolera otrzymuje zwykłe obiekty TypeScript — bez ręcznego dekodowania proto.

Wzorce strumieniowania gRPC w NestJS

Strumieniowanie po stronie serwera pasuje do scenariuszy, w których pojedyncze żądanie generuje wiele wyników w czasie — pomyśl o strumieniach cen w czasie rzeczywistym, paginowanych kursorach bazy danych czy aktualizacjach postępu podczas długo trwającego zadania. Handler zwraca Observable z RxJS, a NestJS strumieniuje każdą wyemitowaną wartość do klienta jako osobną wiadomość gRPC:

users.controller.ts — server streamingtypescript
import { Observable, from } from 'rxjs';
import { map } from 'rxjs/operators';
import { GrpcMethod } from '@nestjs/microservices';

@GrpcMethod('UsersService', 'FindMany')
findMany(data: { role: string; limit: number }): Observable<any> {
  // Stream users matching the filter one by one
  const users$ = from(this.usersService.findByRole(data.role, data.limit));
  return users$.pipe(
    map((user) => ({
      id: user.id,
      email: user.email,
      name: user.name,
      role: user.role,
    })),
  );
}

Strumieniowanie dwukierunkowe korzysta z @GrpcStreamCall(). Zarówno klient, jak i serwer wysyłają niezależne strumienie — przydatne w systemach czatu, edycji współbieżnej czy zwielokrotnionym pozyskiwaniu telemetrii. Handler otrzymuje instancję grpc.ServerDuplexStream i samodzielnie zarządza cyklem odczytu/zapisu.

Obsługa błędów w strumieniowaniu

Obserwator (Observable), który ulegnie awarii wewnątrz strumienia gRPC, kończy całe wywołanie ze statusem CANCELLED. Logikę strumienia należy opakować w operatory catchError i emitować odpowiednie dla gRPC metadane błędu. Nieobsłużone błędy strumienia są przyczyną numer jeden tajemniczych zerwań połączeń w produkcyjnych serwisach NestJS gRPC.

Aplikacje hybrydowe: HTTP i gRPC w jednym serwisie

Wiele rzeczywistych serwisów NestJS potrzebuje zarówno publicznego REST API, jak i wewnętrznych endpointów gRPC. Wzorzec aplikacji hybrydowej łączy wiele transportów z jedną instancją NestJS:

main.ts — hybrid applicationtypescript
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { join } from 'path';
import { AppModule } from './app.module';

async function bootstrap() {
  // HTTP server on port 3000
  const app = await NestFactory.create(AppModule);

  // gRPC microservice on port 5000
  app.connectMicroservice<MicroserviceOptions>({
    transport: Transport.GRPC,
    options: {
      package: 'users',
      protoPath: join(__dirname, 'proto/users.proto'),
      url: '0.0.0.0:5000',
    },
  });

  await app.startAllMicroservices();
  await app.listen(3000);
}
bootstrap();

To podejście pozwala uniknąć wdrażania osobnych serwisów, gdy pojedynczy odgraniczony kontekst potrzebuje zarówno interfejsu zewnętrznego, jak i wewnętrznego. Ten sam kontener wstrzykiwania zależności, strażnicy, interceptory i pipe'y obowiązują dla obu transportów — co ogranicza duplikację i zachowuje spójność zachowania.

Gotowy na rozmowy o Node.js / NestJS?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Granice serwisów i Domain-Driven Design w NestJS

Rozbicie monolitu na mikroserwisy bez jasnych granic tworzy rozproszony monolit — najgorsze cechy obu architektur. Każdy mikroserwis NestJS powinien posiadać pojedynczy odgraniczony kontekst: własną bazę danych, własne modele domenowe i własny cykl życia wdrożenia.

Praktyczne wskazówki dotyczące wyznaczania granic serwisów w NestJS:

  • Jeden moduł NestJS na korzeń agregatu. Jeśli Order i OrderLine zawsze zmieniają się razem, należą do tego samego serwisu. Jeśli User i Order zmieniają się niezależnie, zasługują na osobne serwisy.
  • Zdarzenia asynchroniczne dla efektów ubocznych między domenami. Gdy zostaje złożone zamówienie, należy wyemitować zdarzenie order.created przez Kafkę lub RabbitMQ. Serwis magazynowy reaguje, a serwis zamówień nie musi wiedzieć o jego istnieniu.
  • Współdzielone pakiety proto dla kontraktów. Pliki .proto należy przechowywać we wspólnym repozytorium. Zarówno producent, jak i konsument generują typy z tego samego źródła prawdy — rozjazd staje się błędem CI, a nie incydentem produkcyjnym.

Opcje mikroserwisów z kontenera DI w NestJS 11 sprawiają, że konfiguracja serwisu jest testowalna. Zamiast wpisywać na sztywno adresy brokera w main.ts, można wstrzyknąć ConfigService i podmieniać ciągi połączeń per środowisko bez przebudowy.

Wzorce niezawodności: timeouty, ponowienia i bezpieczniki

Wewnętrzne wywołania gRPC bez terminów (deadline) zamieniają pojedynczą wolną zależność w awarię całego systemu. Każde wywołanie ClientProxy.send() powinno zawierać timeout:

orders.service.tstypescript
import { Inject, Injectable } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { firstValueFrom, timeout, retry } from 'rxjs';

@Injectable()
export class OrdersService {
  private usersService: any;

  constructor(@Inject('USERS_PACKAGE') private client: ClientGrpc) {}

  onModuleInit() {
    this.usersService = this.client.getService('UsersService');
  }

  async getOrderWithUser(orderId: string, userId: string) {
    // 3-second deadline, 2 retries with exponential backoff
    const user = await firstValueFrom(
      this.usersService.findOne({ id: userId }).pipe(
        timeout(3000),
        retry({ count: 2, delay: (err, retryCount) => {
          const jitter = Math.random() * 100;
          return new Promise(r => setTimeout(r, 1000 * retryCount + jitter));
        }}),
      ),
    );
    return { orderId, user };
  }
}

Ponowienia powinny dotyczyć wyłącznie operacji idempotentnych. Ponowienie wywołania CreateOrder grozi powstaniem duplikatów zamówień. Ponowienie zapytania FindOne jest bezpieczne, ponieważ nie wywołuje efektów ubocznych.

W zakresie bezpieczników (circuit breaker) biblioteka opossum dobrze integruje się z NestJS. Wywołania ClientGrpc należy opakować w bezpiecznik, który otwiera się po N kolejnych awariach i zamyka po konfigurowalnym czasie resetu.

Propagacja trace ID w NestJS 11

NestJS 11 automatycznie propaguje trace ID przez transporty Kafka, RabbitMQ i gRPC. W połączeniu z OpenTelemetry umożliwia to pełne śledzenie rozproszone bez niestandardowych interceptorów — znacząca poprawa względem NestJS 10, gdzie wymagana była ręczna propagacja kontekstu.

Pytania rekrutacyjne o mikroserwisy NestJS

Rozmowy techniczne na stanowiska backendowe coraz częściej sprawdzają wiedzę o mikroserwisach obok wzorców specyficznych dla frameworka. Poniższe pytania obejmują koncepcje najczęściej poruszane na rozmowach skupionych na NestJS.

Jaka jest różnica między @MessagePattern a @EventPattern?

@MessagePattern realizuje żądanie-odpowiedź: klient wysyła wiadomość i blokuje się, aż otrzyma odpowiedź. @EventPattern realizuje model fire-and-forget: klient emituje zdarzenie i natychmiast kontynuuje wykonanie. Pod spodem @MessagePattern zwraca wynik handlera przez transport, podczas gdy @EventPattern odrzuca wartości zwracane.

Jak NestJS ładuje i wiąże pliki .proto dla gRPC?

NestJS używa pakietu @grpc/proto-loader do parsowania plików .proto przy starcie i generowania deskryptorów serwisów. Konfiguracja GrpcOptions określa ścieżkę proto i nazwę pakietu. Kontrolery oznaczone @GrpcMethod('ServiceName', 'MethodName') są dopasowywane do sparsowanych deskryptorów. Jeśli metoda zadeklarowana w proto nie ma pasującego handlera, NestJS zgłasza błąd przy bootstrapie.

Kiedy gRPC powinno zastąpić REST między mikroserwisami?

gRPC pasuje do wewnętrznych wywołań serwis-serwis, gdzie liczą się bezpieczeństwo typów, wydajność i strumieniowanie. Protocol Buffers tworzą mniejsze ładunki i szybszą serializację niż JSON. Multipleksowanie HTTP/2 zmniejsza narzut połączeń. REST pozostaje preferowany dla publicznych API, klientów przeglądarkowych i integracji zewnętrznych, gdzie czytelność dla człowieka i szerokie wsparcie narzędzi przeważają nad surową wydajnością.

Jak działa wzorzec aplikacji hybrydowej?

NestFactory.create() uruchamia serwer HTTP. Wywołanie app.connectMicroservice() dołącza dodatkowe transporty — gRPC, Kafkę, Redisa lub dowolny wspierany transporter — do tej samej instancji aplikacji NestJS. Wszystkie transporty dzielą to samo drzewo modułów, kontener DI i potok middleware. app.startAllMicroservices() uruchamia wszystkie podłączone transportery, a app.listen() uruchamia warstwę HTTP.

Jakie wzorce niezawodności zapobiegają kaskadowym awariom?

Najważniejsze są trzy wzorce: timeouty (każde wywołanie RPC potrzebuje terminu), ponowienia z jitterem (tylko dla operacji idempotentnych, z wykładniczym backoffem plus losowym jitterem, aby uniknąć efektu stada), oraz bezpieczniki (otwarte po N awariach, półotwarte po okresie schłodzenia, zamknięte po pomyślnych testach zdrowotności). Integracja NestJS z RxJS sprawia, że timeouty i ponowienia można komponować przez operatory pipe.

Dla głębszej praktyki we wzorcach architektury NestJS moduł moduły i wstrzykiwanie zależności w NestJS omawia podstawy DI, które stanowią fundament kompozycji mikroserwisów. Moduł middleware i interceptory zajmuje się zagadnieniami przekrojowymi współdzielonymi między transportami.

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Podsumowanie

  • Abstrakcja transportowa NestJS 11 pozwala serwisom przełączać się między TCP, gRPC, Kafką i NATS bez przepisywania logiki handlerów — dekoratory @MessagePattern i @EventPattern działają identycznie we wszystkich transportach
  • gRPC z Protocol Buffers wymusza ścisłe kontrakty między serwisami poprzez pliki .proto, wychwytując błędy integracyjne na etapie kompilacji, a nie w produkcji
  • Aplikacje hybrydowe łączą HTTP i gRPC w jednej instancji NestJS, eliminując potrzebę wdrażania osobnych serwisów dla interfejsów publicznych i wewnętrznych
  • Strumieniowanie po stronie serwera przez Observable z RxJS umożliwia strumienie danych w czasie rzeczywistym bez złożoności WebSocketów — NestJS mapuje każdą emisję Observable na wiadomość strumienia gRPC
  • Każde wewnętrzne wywołanie RPC wymaga terminu, ponowienia dotyczą wyłącznie operacji idempotentnych, a bezpieczniki zapobiegają temu, by pojedynczy wolny serwis kaskadowo wywołał pełną awarię
  • Granice serwisów wynikają z Domain-Driven Design: jeden odgraniczony kontekst na serwis, zdarzenia asynchroniczne dla komunikacji między domenami, współdzielone pakiety proto dla egzekwowania kontraktów

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Tagi

#nestjs
#microservices
#grpc
#nodejs
#typescript
#architecture

Udostępnij

Powiązane artykuły