Пошук уроків, статей та іншого контенту
Опишете шаблони запит-відповідь і викликатимете віддалені методи через повідомлення.
У NestJS шаблон повідомлення визначає, який обробник має виконатися для вхідного повідомлення мікросервісу.
Шаблон складається з:
ідентифікатора операції;
даних, які передаються обробнику;
відповіді, яку повертає обробник.
Наприклад, шаблон можна описати рядком:
'users.findOne'Або об'єктом:
{ cmd: 'users.findOne' }Об'єктний варіант зручніший, коли потрібно структурувати повідомлення або використовувати кілька властивостей для його маршрутизації.
Для обробки повідомлення використовується декоратор @MessagePattern().
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
@Controller()
export class UsersController {
@MessagePattern({ cmd: 'users.findOne' })
findOne(id: number) {
return {
id,
name: 'Alice',
};
}
}Коли мікросервіс отримує повідомлення з шаблоном { cmd: 'users.findOne' }, NestJS викликає метод findOne.
Дані повідомлення передаються в метод як аргумент:
@MessagePattern({ cmd: 'users.findOne' })
findOne(id: number) {
// id — це payload повідомлення
return this.usersService.findOne(id);
}Клієнт має надіслати повідомлення з таким самим шаблоном:
client.send({ cmd: 'users.findOne' }, 10);У цьому прикладі:
{ cmd: 'users.findOne' } — шаблон;
10 — payload;
результат методу findOne — відповідь.
ClientProxyДля надсилання повідомлень використовується ClientProxy.
Метод send() застосовується для шаблону запит-відповідь:
const response$ = client.send(
{ cmd: 'users.findOne' },
10,
);Метод send() повертає Observable. Щоб отримати значення з нього в async-методі, зручно використати firstValueFrom():
import { firstValueFrom } from 'rxjs';
const user = await firstValueFrom(
client.send({ cmd: 'users.findOne' }, 10),
);Тип результату можна вказати через узагальнення TypeScript:
const user = await firstValueFrom(
client.send<UserResponse, number>(
{ cmd: 'users.findOne' },
10,
),
);Нижче наведено мікросервіс користувачів, який працює через TCP-транспорт.
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
interface User {
id: number;
name: string;
email: string;
}
@Controller()
export class UsersController {
private readonly users: User[] = [
{
id: 1,
name: 'Alice',
email: 'alice@example.com',
},
{
id: 2,
name: 'Bob',
email: 'bob@example.com',
},
];
@MessagePattern({ cmd: 'users.findOne' })
findOne(id: number): User | null {
// Повертаємо користувача за ідентифікатором
return this.users.find((user) => user.id === id) ?? null;
}
@MessagePattern({ cmd: 'users.findAll' })
findAll(): User[] {
// Повертаємо всіх користувачів
return this.users;
}
}import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
@Module({
controllers: [UsersController],
})
export class AppModule {}import { NestFactory } from '@nestjs/core';
import {
MicroserviceOptions,
Transport,
} from '@nestjs/microservices';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.TCP,
options: {
host: '127.0.0.1',
port: 8877,
},
},
);
await app.listen();
}
bootstrap();Після запуску цей застосунок очікує повідомлення на TCP-порті 8877.
Клієнтський застосунок може зареєструвати підключення до мікросервісу через ClientsModule.
import { Module } from '@nestjs/common';
import {
ClientsModule,
Transport,
} from '@nestjs/microservices';
import { UsersGatewayController } from './users-gateway.controller';
@Module({
imports: [
ClientsModule.register([
{
name: 'USERS_SERVICE',
transport: Transport.TCP,
options: {
host: '127.0.0.1',
port: 8877,
},
},
]),
],
controllers: [UsersGatewayController],
})
export class AppModule {}Значення name використовується для отримання клієнта через @Inject().
import {
Controller,
Get,
Inject,
Param,
ParseIntPipe,
} from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { firstValueFrom } from 'rxjs';
interface User {
id: number;
name: string;
email: string;
}
@Controller('users')
export class UsersGatewayController {
constructor(
@Inject('USERS_SERVICE')
private readonly usersClient: ClientProxy,
) {}
@Get(':id')
async findOne(
@Param('id', ParseIntPipe) id: number,
): Promise<User | null> {
// Віддалено викликаємо обробник users.findOne
return firstValueFrom(
this.usersClient.send<User | null, number>(
{ cmd: 'users.findOne' },
id,
),
);
}
@Get()
async findAll(): Promise<User[]> {
// Викликаємо інший віддалений обробник без payload
return firstValueFrom(
this.usersClient.send<User[], undefined>(
{ cmd: 'users.findAll' },
undefined,
),
);
}
}Тепер HTTP-запит:
GET /users/1спричиняє таку послідовність:
HTTP-контролер отримує параметр id.
ClientProxy надсилає повідомлення { cmd: 'users.findOne' }.
Мікросервіс знаходить метод із відповідним @MessagePattern().
Метод отримує id.
Результат повертається клієнту через TCP.
HTTP-контролер повертає отримані дані як HTTP-відповідь.
Таким чином, HTTP-контролер викликає віддалений метод майже так само, як локальний метод сервісу.
Payload може бути простим значенням:
client.send({ cmd: 'users.findOne' }, 10);Але для складніших операцій краще передавати об'єкт:
client.send(
{ cmd: 'users.search' },
{
query: 'alice',
limit: 10,
},
);Відповідний обробник:
import { MessagePattern } from '@nestjs/microservices';
interface SearchUsersDto {
query: string;
limit: number;
}
@MessagePattern({ cmd: 'users.search' })
searchUsers(payload: SearchUsersDto) {
const { query, limit } = payload;
// Виконуємо пошук із використанням параметрів payload
return {
query,
limit,
items: [],
};
}Для об'єктного payload зручно створювати DTO та застосовувати до нього звичайну валідацію NestJS. Важливо, щоб клієнт і мікросервіс узгоджено використовували структуру даних.
Щоб не дублювати рядки шаблонів у різних застосунках, їх можна винести в константи:
export const USER_PATTERNS = {
findOne: { cmd: 'users.findOne' },
findAll: { cmd: 'users.findAll' },
} as const;Мікросервіс використовує цю константу:
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
import { USER_PATTERNS } from './user-patterns';
@Controller()
export class UsersController {
@MessagePattern(USER_PATTERNS.findOne)
findOne(id: number) {
return {
id,
name: 'Alice',
};
}
}Клієнт використовує той самий шаблон:
import { Inject, Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { firstValueFrom } from 'rxjs';
import { USER_PATTERNS } from './user-patterns';
@Injectable()
export class UsersClient {
constructor(
@Inject('USERS_SERVICE')
private readonly client: ClientProxy,
) {}
async findOne(id: number) {
return firstValueFrom(
this.client.send(USER_PATTERNS.findOne, id),
);
}
}Спільні константи зменшують ризик помилки, коли шаблон на клієнті відрізняється від шаблону на сервері.
Якщо обробник повідомлення викидає помилку, клієнт може отримати її під час очікування Observable:
import { BadRequestException } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
@MessagePattern({ cmd: 'users.findOne' })
findOne(id: number) {
if (!Number.isInteger(id) || id <= 0) {
throw new BadRequestException('Некоректний ідентифікатор');
}
return {
id,
name: 'Alice',
};
}На стороні клієнта можна перехопити помилку через try...catch:
import { firstValueFrom } from 'rxjs';
async function loadUser(client: ClientProxy, id: number) {
try {
return await firstValueFrom(
client.send({ cmd: 'users.findOne' }, id),
);
} catch (error) {
// Обробляємо помилку віддаленого виклику
throw error;
}
}Для бізнес-помилок краще повертати узгоджену структуру відповіді або використовувати винятки NestJS на стороні мікросервісу. Клієнт має бути готовим до того, що віддалений виклик може завершитися помилкою або не отримати відповіді через недоступність сервісу.
ClientProxy зазвичай підключається ліниво — під час першого надсилання повідомлення. Якщо потрібно перевірити підключення під час запуску застосунку, можна явно викликати connect():
import { Inject, Injectable, OnApplicationBootstrap } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
@Injectable()
export class UsersClientConnection
implements OnApplicationBootstrap
{
constructor(
@Inject('USERS_SERVICE')
private readonly client: ClientProxy,
) {}
async onApplicationBootstrap() {
// Перевіряємо підключення до мікросервісу під час запуску
await this.client.connect();
}
}Це дає змогу виявити проблему з підключенням під час запуску, а не лише під час першого запиту користувача.
Клієнт:
client.send({ cmd: 'users.find' }, 1);Мікросервіс:
@MessagePattern({ cmd: 'users.findOne' })
findOne(id: number) {
return id;
}Ці шаблони різні, тому обробник findOne() не буде викликаний.
emit() для запит-відповідьМетод emit() призначений для подій, коли клієнт не очікує результату обробки. Для виклику віддаленого методу з відповіддю використовуйте send():
const result$ = client.send(
{ cmd: 'users.findOne' },
1,
);firstValueFrom()send() повертає Observable, а не готове значення:
const user = client.send({ cmd: 'users.findOne' }, 1);Якщо метод оголошений як async, потрібно отримати значення з Observable:
const user = await firstValueFrom(
client.send({ cmd: 'users.findOne' }, 1),
);Якщо обробник очікує об'єкт:
@MessagePattern({ cmd: 'users.search' })
search(payload: { query: string; limit: number }) {
return payload;
}не можна надсилати лише рядок:
client.send({ cmd: 'users.search' }, 'alice');Потрібно передати всі очікувані поля:
client.send(
{ cmd: 'users.search' },
{
query: 'alice',
limit: 10,
},
);Якщо ClientProxy реєструється через ClientsModule, цей модуль має бути імпортований у модуль, де використовується ін'єкція клієнта:
@Module({
imports: [
ClientsModule.register([
{
name: 'USERS_SERVICE',
transport: Transport.TCP,
options: {
host: '127.0.0.1',
port: 8877,
},
},
]),
],
})
export class AppModule {}@MessagePattern() пов'язує шаблон повідомлення з методом мікросервісу.
ClientProxy.send() використовується для запитів, які повинні повернути відповідь.
send() повертає Observable, тому в async-методах зазвичай використовується firstValueFrom().
Шаблон на клієнті має точно відповідати шаблону обробника.
Payload може бути простим значенням або структурованим об'єктом.
Спільні константи шаблонів допомагають узгодити клієнт і мікросервіс.
Через шаблони повідомлень один NestJS-застосунок може викликати методи іншого застосунку через ClientProxy.