Пошук уроків, статей та іншого контенту
Створите мікросервіс і клієнта, налаштуєте повідомлення та порівняєте request-response і event-based взаємодію.
Мікросервіс — це окремий застосунок, який виконує обмежену бізнес-функцію та взаємодіє з іншими застосунками через повідомлення.
У NestJS мікросервіс:
запускається окремим процесом;
може не мати HTTP-сервера;
отримує повідомлення через транспорт;
обробляє повідомлення за визначеним патерном;
може повертати відповідь або лише повідомляти про подію.
Для обміну повідомленнями NestJS підтримує різні транспорти, зокрема:
TCP;
Redis;
NATS;
MQTT;
RabbitMQ;
Kafka.
У цьому уроці використаємо TCP, оскільки його легко запустити локально без окремого брокера повідомлень.
NestJS розрізняє два основні типи повідомлень.
Клієнт надсилає повідомлення та очікує на відповідь.
Такий підхід підходить, коли:
результат операції потрібен одразу;
клієнт не може продовжити роботу без відповіді;
операція має поводитися як виклик методу.
Для request-response використовують:
на стороні мікросервісу — @MessagePattern();
на стороні клієнта — client.send().
Клієнт публікує подію, не очікуючи бізнес-відповіді.
Такий підхід підходить, коли:
кілька сервісів можуть реагувати на одну подію;
автору події не потрібен результат обробки;
обробку можна виконати асинхронно;
потрібно зменшити зв’язність між сервісами.
Для подій використовують:
на стороні мікросервісу — @EventPattern();
на стороні клієнта — client.emit().
Створимо два NestJS-застосунки:
orders-service/
src/
app.module.ts
orders.controller.ts
main.ts
api-client/
src/
app.module.ts
orders-client.controller.ts
main.tsorders-service буде мікросервісом, який слухає TCP-порт 8877.
api-client буде звичайним HTTP-застосунком. Він прийматиме HTTP-запити та пересилатиме їх до мікросервісу.
Встановіть залежність у кожному застосунку:
npm install @nestjs/microservicesУ проєкті NestJS пакет rxjs зазвичай уже встановлений. Якщо його немає, встановіть також:
npm install rxjsorders-service/src/main.tsimport { 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();createMicroservice() створює NestJS-застосунок, який не слухає HTTP-запити, а приймає повідомлення через вказаний транспорт.
У цьому прикладі:
Transport.TCP визначає транспорт;
host задає адресу прослуховування;
port задає TCP-порт;
app.listen() запускає мікросервіс.
orders-service/src/app.module.tsimport { Module } from '@nestjs/common';
import { OrdersController } from './orders.controller';
@Module({
controllers: [OrdersController],
})
export class AppModule {}orders-service/src/orders.controller.tsimport {
Controller,
Logger,
} from '@nestjs/common';
import {
EventPattern,
MessagePattern,
Payload,
} from '@nestjs/microservices';
interface CalculateTotalMessage {
price: number;
quantity: number;
}
interface OrderCreatedMessage {
orderId: string;
customerId: string;
total: number;
}
@Controller()
export class OrdersController {
private readonly logger = new Logger(OrdersController.name);
@MessagePattern('orders.calculate-total')
calculateTotal(
@Payload() message: CalculateTotalMessage,
): number {
return message.price * message.quantity;
}
@EventPattern('orders.created')
handleOrderCreated(
@Payload() message: OrderCreatedMessage,
): void {
this.logger.log(
`Отримано подію створення замовлення ${message.orderId}`,
);
this.logger.log(
`Клієнт: ${message.customerId}, сума: ${message.total}`,
);
}
}@MessagePattern('orders.calculate-total') реєструє обробник request-response повідомлення.
Коли клієнт надішле повідомлення з патерном orders.calculate-total, NestJS викличе метод calculateTotal(). Значення, яке повертає цей метод, буде відповіддю клієнту.
@EventPattern('orders.created') реєструє обробник події. Метод handleOrderCreated() не повертає бізнес-відповідь відправнику.
Назва патерну є частиною контракту між клієнтом і мікросервісом. Вона має збігатися на обох сторонах:
client.send('orders.calculate-total', payload);і:
@MessagePattern('orders.calculate-total')Клієнтський застосунок матиме HTTP-контролер. Він перетворюватиме HTTP-запити на повідомлення до orders-service.
api-client/src/main.tsimport { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();api-client/src/app.module.tsimport { Module } from '@nestjs/common';
import {
ClientsModule,
Transport,
} from '@nestjs/microservices';
import { OrdersClientController } from './orders-client.controller';
export const ORDERS_SERVICE = 'ORDERS_SERVICE';
@Module({
imports: [
ClientsModule.register([
{
name: ORDERS_SERVICE,
transport: Transport.TCP,
options: {
host: '127.0.0.1',
port: 8877,
},
},
]),
],
controllers: [OrdersClientController],
})
export class AppModule {}ClientsModule.register() створює клієнт для підключення до мікросервісу.
Властивість name — це токен, за яким клієнт буде отримано через dependency injection:
@Inject(ORDERS_SERVICE)
private readonly ordersClient: ClientProxyПараметри TCP у клієнта мають відповідати параметрам TCP у мікросервісу:
host: '127.0.0.1',
port: 8877,api-client/src/orders-client.controller.tsimport {
Body,
Controller,
Inject,
Post,
} from '@nestjs/common';
import {
ClientProxy,
} from '@nestjs/microservices';
import { firstValueFrom } from 'rxjs';
import { ORDERS_SERVICE } from './app.module';
interface CalculateTotalBody {
price: number;
quantity: number;
}
interface OrderCreatedBody {
orderId: string;
customerId: string;
total: number;
}
@Controller('orders')
export class OrdersClientController {
constructor(
@Inject(ORDERS_SERVICE)
private readonly ordersClient: ClientProxy,
) {}
@Post('calculate-total')
async calculateTotal(
@Body() body: CalculateTotalBody,
): Promise<{ total: number }> {
const total = await firstValueFrom(
this.ordersClient.send<number>(
'orders.calculate-total',
body,
),
);
return { total };
}
@Post('created')
async publishOrderCreated(
@Body() body: OrderCreatedBody,
): Promise<{ published: boolean }> {
await firstValueFrom(
this.ordersClient.emit('orders.created', body),
);
return { published: true };
}
}Метод ClientProxy.send() повертає Observable.
У прикладі:
const total = await firstValueFrom(
this.ordersClient.send<number>(
'orders.calculate-total',
body,
),
);Відбувається така послідовність:
HTTP-клієнт надсилає запит до api-client.
HTTP-контролер викликає ordersClient.send().
TCP-клієнт надсилає повідомлення до orders-service.
NestJS знаходить обробник @MessagePattern('orders.calculate-total').
Мікросервіс обчислює суму.
Результат повертається клієнту.
HTTP-контролер повертає результат HTTP-клієнту.
Виклик firstValueFrom() перетворює Observable на Promise, щоб використати його разом із async/await.
Приклад запиту:
curl -X POST http://localhost:3000/orders/calculate-total \
-H "Content-Type: application/json" \
-d '{"price":25,"quantity":4}'Відповідь:
{
"total": 100
}У цьому сценарії клієнт залежить від відповіді мікросервісу. Якщо мікросервіс недоступний або не повертає результат, HTTP-запит не зможе нормально завершитися.
Для надсилання події використовується ClientProxy.emit():
await firstValueFrom(
this.ordersClient.emit('orders.created', body),
);Мікросервіс отримає подію в обробнику:
@EventPattern('orders.created')
handleOrderCreated(@Payload() message: OrderCreatedMessage): void {
// Обробка події
}Приклад запиту:
curl -X POST http://localhost:3000/orders/created \
-H "Content-Type: application/json" \
-d '{"orderId":"order-42","customerId":"customer-7","total":100}'Відповідь HTTP-клієнту:
{
"published": true
}Ця відповідь означає, що клієнт опублікував подію. Вона не є результатом виконання бізнес-логіки в обробнику події.
Після отримання події мікросервіс виведе повідомлення в журнал:
Отримано подію створення замовлення order-42
Клієнт: customer-7, сума: 100Спочатку запустіть мікросервіс:
cd orders-service
npm run start:devПісля цього в іншому терміналі запустіть HTTP-клієнт:
cd api-client
npm run start:devПорядок важливий для першого запиту: orders-service має слухати TCP-порт 8877.
У production-застосунках також потрібно передбачити обробку ситуацій, коли мікросервіс тимчасово недоступний. На локальному прикладі достатньо переконатися, що обидва процеси запущені.
const result = await firstValueFrom(
client.send('orders.calculate-total', payload),
);Властивості:
є конкретний відправник і очікувана відповідь;
результат можна повернути HTTP-клієнту;
помилка мікросервісу впливає на поточний запит;
підходить для запитів і команд, яким потрібен результат.
await firstValueFrom(
client.emit('orders.created', payload),
);Властивості:
відправник публікує факт, що щось відбулося;
бізнес-відповідь від обробника не очікується;
один тип події можуть обробляти різні мікросервіси;
підходить для реакції на події та асинхронних процесів.
Вибір підходу залежить від семантики операції:
«Обчисли й поверни результат» — request-response.
«Замовлення створено, відреагуй на це» — event-based.
Патерн визначає, який обробник має отримати повідомлення.
У простому випадку патерн є рядком:
'orders.calculate-total'NestJS також підтримує структуровані патерни, наприклад об’єкти:
@MessagePattern({
cmd: 'calculate-total',
})
calculateTotal(@Payload() message: CalculateTotalMessage) {
return message.price * message.quantity;
}Клієнт у такому разі має використовувати такий самий патерн:
client.send(
{ cmd: 'calculate-total' },
payload,
);Структуровані патерни можуть бути корисними, коли потрібно явно розділити команду та інші властивості повідомлення. Незалежно від форми патерну, клієнт і мікросервіс повинні використовувати однаковий контракт.
ClientProxyClientProxy є абстракцією клієнта мікросервісу. Він відповідає за:
встановлення з'єднання з транспортом;
серіалізацію повідомлення;
надсилання патерну та payload;
отримання відповіді для send();
публікацію подій через emit().
З'єднання зазвичай встановлюється ліниво — під час першої взаємодії з клієнтом.
Для request-response потрібно обробити Observable, наприклад через:
const result = await firstValueFrom(
client.send('some-pattern', payload),
);Для події також важливо підписатися на Observable. У прикладі це робить firstValueFrom():
await firstValueFrom(
client.emit('some-event', payload),
);Без підписки операція з Observable може не бути виконана.
Клієнт і мікросервіс мають узгодити:
назву патерну;
структуру payload;
типи властивостей;
обов'язкові та необов'язкові поля;
формат результату для request-response;
поведінку в разі помилки.
Наприклад, для повідомлення:
{
"price": 25,
"quantity": 4
}мікросервіс очікує числові price і quantity.
Якщо контракт змінюється, потрібно синхронно оновити клієнта та обробник мікросервісу. У більших системах типи повідомлень часто виносять у спільний пакет, але цей підхід має бути частиною окремої архітектурної домовленості.
Клієнт:
client.send('orders.calculate', payload);Мікросервіс:
@MessagePattern('orders.calculate-total')Ці патерни різні, тому обробник не буде викликаний. Назва має збігатися повністю.
send() для подіїЯкщо операція не має повертати результат, не слід моделювати її як request-response:
client.send('orders.created', payload);Для події використовуйте:
client.emit('orders.created', payload);emit() там, де потрібен результатЯкщо HTTP-контролеру потрібно отримати обчислену суму, emit() не підходить. Використовуйте send() і повернення значення з @MessagePattern().
Клієнт і мікросервіс повинні використовувати той самий порт:
port: 8877Якщо мікросервіс слухає інший порт, клієнт не зможе підключитися.
ObservableОсь недостатній варіант:
this.ordersClient.emit('orders.created', payload);Потрібно дочекатися або підписатися на результат:
await firstValueFrom(
this.ordersClient.emit('orders.created', payload),
);Обробник @EventPattern() призначений для реакції на подію, а не для повернення результату відправнику. Якщо результат є необхідною частиною операції, використовуйте @MessagePattern().
Мікросервіс NestJS може працювати окремим процесом і взаємодіяти з клієнтами через транспорт.
@MessagePattern() використовується для request-response повідомлень.
@EventPattern() використовується для подій.
ClientProxy.send() надсилає повідомлення й очікує відповідь.
ClientProxy.emit() публікує подію без очікування бізнес-відповіді.
Для роботи з ClientProxy використовується ClientsModule.register().
Патерн повідомлення та структура payload мають бути однаковими на стороні клієнта й мікросервісу.
Request-response підходить для операцій, яким потрібен результат.
Event-based підхід підходить для асинхронної реакції на факт, що вже відбувся.