NestJS — фреймворк, который значительно упрощает создание серверных приложений на Node.js. Его прелесть в том, что он комбинирует концепции ООП, функционального программирования и предлагает архитектуру, вдохновленную Angular, делая разработку микросервисов интуитивно понятной для многих разработчиков. В паре с TypeScript этот фреймворк становится мощным инструментом для построения надежных микросервисных систем.
У микросервисов есть свои особенности — каждый сервис работает как отдельное маленькое приложение со своей ответственностью и логикой. Взаимодействие между ними — отдельная история, которая требует продуманного подхода. Здесь на сцену выходит TCP (Transmission Control Protocol) — надежный протокол, который может стать основой коммуникации между вашими микросервисами.
По мере роста проектов все чаще возникает необходимость отхода от монолитной архитектуры. Это как переход от огромного танкера к флотилии маленьких, но маневренных кораблей — каждый отвечает за свою задачу, но вместе они формируют мощную систему. Микросервисы позволяют командам разработчиков работать над отдельными компонентами системы независимо, ускоряя цикл разработки и облегчая внедрение новых функций. В реальной жизни разработка микросервисов выглядит примерно так: сначала определяете границы ответственности каждого сервиса, затем реализуете их как небольшие, независимые приложения, а потом настраиваете коммуникацию между ними. Звучит просто, но дьявол, как всегда, кроется в деталях. Нужно правильно выбрать протоколы коммуникации, решить вопросы отказоустойчивости, обеспечить мониторинг — и это только верхушка айсберга.
В этой статье мы разберем, как построить работающую микросервисную архитектуру с использованием NestJS, TypeScript и TCP в качестве транспортного протокола. Мы пройдем весь путь: от теории и основ до практической реализации, включая настройку проекта, создание сервисов, организацию коммуникации между ними и решение типичных проблем, с которыми сталкиваются разработчики при работе с микросервисами.
Основы микросервисной архитектуры
Для полного понимания микросервисной архитектуры полезно сопоставить её с классическим монолитным подходом. Представьте, что монолит — это огромный механизм, где все шестерёнки находятся в одном корпусе. При поломке одной детали приходится останавливать и разбирать всю машину. В микросервисном же подходе каждый элемент функционирует автономно. Если выходит из строя один "механизм", остальные продолжают работу.
Монолитное приложение представляет собой единое целое — код, данные, бизнес-логика упакованы в одну большую программу. Такие приложения проще создавать на начальных этапах, но с ростом кодовой базы возникают серьёзные проблемы: сложность поддержки, трудности при внедрении изменений и ограничения масштабирования.
Микросервисная архитектура разбивает приложение на набор отдельных сервисов, каждый из которых:- Обслуживает конкретную бизнес-функцию.
- Разрабатывается независимо от других сервисов.
- Имеет собственную базу данных (если необходимо).
- Коммуницирует с другими сервисами посредством сетевых протоколов.
Такой подход обеспечивает гибкость разработки и развёртывания. Например, команда, отвечающая за аутентификацию пользователей, может выпускать обновления независимо от команды, работающей над функционалом оплаты. Это критически важно для крупных организаций, где над разными частями системы трудятся отдельные команды.
При выборе протоколов коммуникации между сервисами многие разработчики первым делом обращаются к REST API. Однако в определённых сценариях HTTP может быть не лучшим выбором. Здесь на сцену выходит TCP — базовый протокол, обеспечивающий надёжную передачу данных между узлами сети.
Почему же TCP может быть предпочтительнее HTTP для взаимодействия микросервисов? Причин несколько:
1. Производительность. TCP устанавливает постоянное соединение, что снижает накладные расходы на многократное открытие и закрытие соединений, характерные для HTTP.
2. Двусторонняя связь. TCP позволяет передавать данные в обоих направлениях без создания новых соединений.
3. Контроль потока. TCP имеет встроенные механизмы управления потоком данных, предотвращающие перегрузку сети.
4. Меньший размер сообщений. Отсутствие заголовков HTTP делает сообщения компактнее.
NestJS прекрасно поддерживает TCP как протокол коммуникации между микросервисами через встроенный модуль @nestjs/microservices . Это дает разработчикам гибкость в выборе подходящего протокола без необходимости существенно менять структуру кода. Важно понимать, что выбор между TCP и HTTP не является абсолютным. В одной системе могут сосуществовать оба протокола. Например, внутренние коммуникации между сервисами могут идти по TCP, а внешний API для клиентов предоставляться по HTTP.
При проектировании микросервисной архитектуры активно применяются различные шаблоны, облегчающие решение типовых задач. Один из важнейших — паттерн API Gateway (шлюз API). Это специализированный сервис, который выступает единой точкой входа для клиентских приложений, маршрутизируя запросы к соответствующим микросервисам. Такой подход избавляет клиентов от необходимости знать о внутренней структуре системы и упрощает процессы аутентификации и авторизации.
Другой распространенный паттерн — Circuit Breaker (прерыватель цепи). Его основная идея заключается в защите системы от каскадных сбоев. Если какой-то сервис начинает работать некорректно, "прерыватель" временно блокирует запросы к нему, возвращая заранее определенный ответ или переключаясь на резервный вариант. После восстановления работы сервиса прерыватель автоматически возобновляет нормальную маршрутизацию. Это существенно повышает отказоустойчивость системы.
При построении микросервисной архитектуры особую важность приобретает обнаружение сервисов (Service Discovery). Поскольку сервисы могут динамически масштабироваться, появляться и исчезать, другие сервисы должны иметь способ найти их текущие экземпляры. Здесь помогают специальные реестры сервисов, которые хранят информацию о местоположении каждого сервиса.
Для наиболее эффективного использования TypeScript в микросервисной архитектуре рекомендуется создавать общие библиотеки типов. Это позволяет всем сервисам использовать одинаковые интерфейсы при взаимодействии. Причем эти типы могут быть не только для данных, но и для API-контрактов между сервисами, например, имен событий или шаблонов сообщений.
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // shared/src/lib/dto/user.dto.ts
export class User {
id?: number;
username: string;
email: string;
isActive: boolean;
}
// shared/src/lib/patterns/auth.patterns.ts
export const AUTH_SERVICE_PATTERNS = {
CREATE_USER: 'create_user',
GET_USER: 'get_user',
VALIDATE_USER: 'validate_user'
} |
|
Такой подход существенно упрощает типобезопасное взаимодействие между сервисами и помогает отлавливать потенциальные ошибки уже на этапе компиляции.
Еще один важный аспект микросервисной архитектуры — обработка распределенных транзакций. В монолитных приложениях транзакционность обычно обеспечивается базой данных. Однако когда операция затрагивает несколько сервисов с отдельными базами данных, необходимы специальные механизмы. Один из подходов — паттерн Saga, который представляет транзакцию как последовательность локальных транзакций в отдельных сервисах с компенсирующими действиями в случае сбоя.
NestJS предлагает удобные механизмы для реализации этих паттернов. Например, для реализации паттерна Saga можно использовать комбинацию событий и обработчиков:
TypeScript | 1
2
3
4
5
6
7
8
9
10
| @EventPattern('order.created')
handleOrderCreated(data: OrderCreatedEvent) {
try {
// Обработка заказа
this.eventBus.emit('payment.process', { orderId: data.id });
} catch (error) {
// Компенсирующее действие
this.eventBus.emit('order.cancel', { orderId: data.id });
}
} |
|
Когда мы говорим о коммуникации между микросервисами, важно упомянуть о стилях взаимодействия. В NestJS поддерживаются два основных подхода: запрос-ответ и публикация-подписка.
Паттерн запрос-ответ (request-response) идеально подходит для ситуаций, когда клиент ожидает конкретного ответа от сервиса. В NestJS это реализуется с помощью метода send() у клиента микросервиса:
TypeScript | 1
| const user = await this.authClient.send('get_user', userDto).toPromise(); |
|
С другой стороны, паттерн публикация-подписка (publish-subscribe) отлично работает для асинхронного взаимодействия, когда сервис публикует событие, не заботясь о том, кто и как его обработает. В этом случае в NestJS используется метод emit() :
TypeScript | 1
| this.paymentClient.emit('payment.processed', { orderId: '123', status: 'success' }); |
|
Асинхронное взаимодействие между микросервисами через события может значительно уменьшить связность системы. Сервисы не привязаны напрямую друг к другу и могут развиваться более независимо, что соответствует принципам микросервисной архитектуры. Еще одним важным аспектом является сериализация данных при обмене сообщениями. По умолчанию NestJS использует JSON, но в случаях, когда производительность критична, можно рассмотреть более эффективные форматы, такие как Protocol Buffers или MessagePack.
При работе с микросервисами неизбежно возникает проблема версионирования API. Когда один сервис меняется, необходимо обеспечить обратную совместимость или координировать обновление всех зависимых сервисов. Это часто решается через стратегии постепенного развёртывания и чёткие контракты между сервисами. В реальных проектах важно также учитывать вопросы отказоустойчивости. Микросервисная архитектура по своей природе распределенная, а сетевые взаимодействия ненадежны. Поэтому необходимо продумать стратегии обработки таймаутов, повторных попыток и деградации функциональности.
Микросервисы на базе NestJS могут использовать различные стратегии повторных попыток при сбоях. Например, с помощью библиотеки RxJS:
TypeScript | 1
2
3
4
5
6
7
| this.httpService.get('http://service-a/resource')
.pipe(
retry(3),
timeout(5000),
catchError(error => of({ fallback: true }))
)
.subscribe(response => this.handleResponse(response)); |
|
При проектировании микросервисной архитектуры также встаёт вопрос о согласованности данных. В отличие от монолитных приложений, где можно полагаться на ACID-транзакции, в микросервисах часто приходится использовать модель согласованности данных в конечном счете (Eventual Consistency). Это значит, что система может временно находиться в несогласованном состоянии, но в итоге придёт к согласованности.
Для эффективного мониторинга и отладки микросервисной архитектуры необходимо внедрять трассировку запросов. Каждый запрос, проходящий через несколько сервисов, должен иметь уникальный идентификатор, позволяющий отследить его путь и время выполнения каждого этапа. Это можно реализовать с помощью механизма перехватчиков (Interceptors) в NestJS:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| @Injectable()
export class TraceIdInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const traceId = request.headers['x-trace-id'] || uuidv4();
// Добавляем traceId в контекст запроса
request.traceId = traceId;
// Добавляем traceId в заголовки ответа
const response = context.switchToHttp().getResponse();
response.header('x-trace-id', traceId);
return next.handle();
}
} |
|
Чтобы обеспечить эффективную работу микросервисной архитектуры, важно тщательно продумать границы микросервисов. Хорошим подходом является выделение микросервисов по бизнес-возможностям, а не по технологическим слоям. Каждый микросервис должен быть автономным и отвечать за конкретную бизнес-функцию.
Микросервисы и их ограничения в вебе Доброго вечер коллеги, хочу обсудить проблему микросервисов в вебе.
У меня есть порядка 15 сервисов 1-го проекта, в данный момент каждый из них... Создаем TCP сервер (на основе сервера MicroBridge LightWeight) Добрый день, Уважаемые разработчики!
Помогите пожалуйста, пытаюсь передать данные между ардуино и телефоном.
С телефона отправляю на ардуино:... Учить новичку JavaScript и Typescript или достаточно одного Typescript? Добрый день.
Изучаю Asp net core. Хочу дополнительно изучить JavaScript и Typescript.
Встал вопрос: есть ли смысл учить JavaScript, если сейчас... NestJS Sequelize Как отключить автоматическое создание поле id, даже если я его не указываю sequelize его всё равно создаёт, причём даже если я его укажу он всё равно...
Настройка проекта
Теперь, когда мы разобрались с теоретическими аспектами, давайте перейдем к практике и настроим наш микросервисный проект. Для основы возьмем NestJS с TypeScript, а для коммуникации между сервисами будем использовать TCP.
Первое, что нам нужно решить — как организовать разработку нескольких сервисов. Вместо создания отдельных репозиториев для каждого микросервиса, мы воспользуемся подходом монорепозитория. Это упростит совместное использование кода, управление зависимостями и позволит сохранить целостность системы.
Идеальным инструментом для создания монорепозитория является Nx. Он предоставляет отличную экосистему для работы с микросервисами в едином репозитории кода. Начнем с инициализации проекта:
Bash | 1
| npx create-nx-workspace nestjs-microservices --preset=nest |
|
При запуске этой команды система попросит указать имя первого приложения. Давайте назовем его api-gateway — это будет наша точка входа, через которую клиенты будут взаимодействовать с микросервисами.
После создания проекта, установим необходимые зависимости:
Bash | 1
2
| cd nestjs-microservices
npm install @nestjs/microservices class-validator class-transformer |
|
Теперь настроим TypeScript для нашего проекта. Nx уже создал базовую конфигурацию, но мы можем её доработать под наши нужды. В файле tsconfig.base.json определим пути для более удобного импорта общих модулей:
JSON | 1
2
3
4
5
6
7
8
| {
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@nestjs-microservices/*": ["libs/*/src/index.ts"]
}
}
} |
|
Для совместного использования кода между сервисами создадим общую библиотеку. В Nx это делается с помощью команды:
Bash | 1
| nx g @nx/nest:lib shared |
|
Эта библиотека будет содержать DTO (Data Transfer Objects), интерфейсы, константы и другие общие компоненты, которые используются разными микросервисами.
Теперь создадим наш первый микросервис — сервис аутентификации:
Bash | 1
| nx g @nx/nest:app auth-microservice |
|
После создания сервисов, нужно настроить структуру каждого из них. Для api-gateway создадим модуль аутентификации:
Bash | 1
2
3
| nx g @nx/nest:module auth --project=api-gateway
nx g @nx/nest:service auth --project=api-gateway
nx g @nx/nest:controller auth --project=api-gateway |
|
В shared библиотеке создадим DTO и интерфейсы для обмена данными между сервисами:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // shared/src/lib/dto/create-user.dto.ts
import { IsNotEmpty, IsString } from 'class-validator';
export class CreateUserDto {
@IsString()
@IsNotEmpty()
username: string;
@IsNotEmpty()
password: string;
}
// shared/src/lib/entities/user.entity.ts
export class User {
id?: number;
username: string;
password: string;
} |
|
Одна из сильных сторон NestJS — его гибкая модульная система, которая идеально подходит для микросервисной архитектуры. Каждый модуль инкапсулирует определенную функциональность и может быть импортирован в другие модули.
При настройке микросервисов в NestJS ключевую роль играет модуль ClientsModule . Он позволяет регистрировать соединения с другими микросервисами и инжектировать их через систему внедрения зависимостей.
Вот как выглядит настройка модуля аутентификации в API Gateway:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // apps/api-gateway/src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
@Module({
imports: [
ClientsModule.register([
{
name: 'AUTH_MICROSERVICE',
transport: Transport.TCP,
options: {
host: 'localhost',
port: 3001,
},
},
]),
],
providers: [AuthService],
controllers: [AuthController],
})
export class AuthModule {} |
|
Здесь мы регистрируем клиента для коммуникации с auth-microservice через TCP. Обратите внимание на параметры host и port — они указывают, где искать микросервис. В реальных проектах эти значения часто выносятся в конфигурацию, чтобы можно было легко переключаться между средами разработки, тестирования и продакшена.
Для микросервисов важно правильно настроить точку входа. Для обычных NestJS приложений обычно используется HTTP-сервер, но для микросервисов мы будем использовать TCP. Вот как настраивается файл main.ts для auth-microservice:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // apps/auth-microservice/src/main.ts
import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { AppModule } from './app/app.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.TCP,
options: {
host: 'localhost',
port: 3001,
},
}
);
await app.listen();
Logger.log('Auth микросервис запущен');
}
bootstrap(); |
|
Метод NestFactory.createMicroservice() создает экземпляр микросервиса вместо стандартного HTTP-приложения. Мы указываем транспорт TCP и настройки для него.
Для управления версиями API и контрактами между микросервисами полезно создать файл с константами шаблонов сообщений:
TypeScript | 1
2
3
4
5
6
| // shared/src/lib/constants/message-patterns.ts
export const AUTH_MESSAGE_PATTERNS = {
GET_USER: 'get_user',
CREATE_USER: 'create_user',
VALIDATE_TOKEN: 'validate_token'
}; |
|
Это обеспечит единый источник истины для имен сообщений, используемых при коммуникации.
При настройке микросервисов стоит также позаботиться о валидации входных данных. NestJS интегрируется с библиотекой class-validator , что позволяет легко добавить валидацию для DTO:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
| // main.ts (api-gateway)
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // Удаляет свойства, которые не определены в DTO
forbidNonWhitelisted: true, // Выбрасывает ошибку при попытке передать неопределенные свойства
transform: true, // Автоматически преобразует примитивные типы
}));
await app.listen(3000);
} |
|
Такой подход обеспечивает надежную проверку входных данных и снижает риск ошибок при передаче данных между сервисами.
Наконец, для удобства разработки настроим скрипты в package.json , которые позволят запускать все микросервисы одной командой:
JSON | 1
2
3
4
5
6
7
| {
"scripts": {
"start:dev": "nx run-many --target=serve --all --parallel",
"start:api-gateway": "nx serve api-gateway",
"start:auth": "nx serve auth-microservice"
}
} |
|
Теперь мы настроили базовую инфраструктуру для нашего микросервисного проекта. У нас есть API Gateway, микросервис аутентификации и общая библиотека для совместного использования кода. В следующих разделах мы углубимся в детали реализации и рассмотрим, как эти компоненты взаимодействуют друг с другом.
Практическая реализация
Давайте реализуем простую, но работающую микросервисную архитектуру с использованием NestJS, TypeScript и TCP. В нашем примере мы создадим два основных компонента:
1. API Gateway — сервис, который будет принимать HTTP-запросы от клиентов и перенаправлять их в соответствующие микросервисы.
2. Auth Microservice — сервис аутентификации, ответственный за регистрацию пользователей и проверку учетных данных.
Начнем с реализации сервиса аутентификации. В этом сервисе нам понадобится хранилище пользователей. Для простоты примера будем хранить данные в памяти:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // apps/auth-microservice/src/app/user.repository.ts
import { Injectable } from '@nestjs/common';
import { CreateUserDto, User } from '@nestjs-microservices/shared';
@Injectable()
export class UserRepository {
private users: User[] = [];
save(user: CreateUserDto): User {
const newUser = new User();
newUser.id = this.users.length + 1;
newUser.username = user.username;
newUser.password = user.password;
this.users.push(newUser);
return newUser;
}
findOne(username: string): User | undefined {
return this.users.find((user) => user.username === username);
}
} |
|
Теперь создадим сервис, который будет использовать это хранилище:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // apps/auth-microservice/src/app/app.service.ts
import { Injectable } from '@nestjs/common';
import { CreateUserDto, User } from '@nestjs-microservices/shared';
import { UserRepository } from './user.repository';
@Injectable()
export class AppService {
constructor(private readonly userRepository: UserRepository) {}
createUser(newUser: CreateUserDto): User {
return this.userRepository.save(newUser);
}
getUser(username: string): User | undefined {
return this.userRepository.findOne(username);
}
} |
|
Далее нам нужен контроллер, который будет обрабатывать сообщения от API Gateway. В микросервисах NestJS для этого используется декоратор @MessagePattern() :
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // apps/auth-microservice/src/app/app.controller.ts
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
import { CreateUserDto, User } from '@nestjs-microservices/shared';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@MessagePattern('get_user')
handleGetUser(user: CreateUserDto) {
return this.appService.getUser(user.username);
}
@MessagePattern('create_user')
handleCreateUser(newUser: CreateUserDto) {
return this.appService.createUser(newUser);
}
} |
|
Декоратор @MessagePattern() указывает, какие паттерны сообщений будет обрабатывать метод. Когда микросервис получает сообщение с паттерном 'get_user', он вызывает метод handleGetUser() .
Теперь перейдем к API Gateway. Сначала создадим сервис, который будет общаться с микросервисом аутентификации:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // apps/api-gateway/src/auth/auth.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { Observable } from 'rxjs';
import { CreateUserDto, User } from '@nestjs-microservices/shared';
@Injectable()
export class AuthService {
constructor(
@Inject('AUTH_MICROSERVICE') private readonly authClient: ClientProxy
) {}
getUser(createUserDto: CreateUserDto): Observable<User> {
return this.authClient.send<User, CreateUserDto>('get_user', createUserDto);
}
createUser(createUserDto: CreateUserDto): Observable<User> {
return this.authClient.send<User, CreateUserDto>('create_user', createUserDto);
}
} |
|
Здесь мы инжектируем клиент микросервиса, который был зарегистрирован в модуле, и используем его для отправки сообщений в микросервис аутентификации. Метод send() работает по принципу запрос-ответ и возвращает Observable, из которого можно получить ответ.
Теперь создадим контроллер API Gateway, который будет обрабатывать HTTP-запросы:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| // apps/api-gateway/src/auth/auth.controller.ts
import { Body, Controller, Post, BadRequestException } from '@nestjs/common';
import { lastValueFrom } from 'rxjs';
import { CreateUserDto, User } from '@nestjs-microservices/shared';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
async login(@Body() createUserDto: CreateUserDto) {
const user: User = await lastValueFrom(
this.authService.getUser(createUserDto),
{ defaultValue: undefined }
);
if (!user) {
throw new BadRequestException('Неверные учетные данные');
}
const isMatch = user.password === createUserDto.password;
if (!isMatch) {
throw new BadRequestException('Неверный пароль');
}
console.log(`Пользователь ${user.username} успешно вошел в систему.`);
return user;
}
@Post('signup')
async signup(@Body() createUserDto: CreateUserDto) {
const user: User = await lastValueFrom(
this.authService.getUser(createUserDto),
{ defaultValue: undefined }
);
if (user) {
throw new BadRequestException(
[INLINE]Пользователь с именем ${createUserDto.username} уже существует![/INLINE]
);
}
return this.authService.createUser(createUserDto);
}
} |
|
В этом контроллере мы используем функцию lastValueFrom() из RxJS для преобразования Observable в Promise, что делает код более читаемым благодаря async/await. Контроллер обрабатывает два маршрута: /auth/login для входа пользователей и /auth/signup для регистрации. Обратите внимание на проверку существования пользователя перед регистрацией — это предотвращает создание дубликатов. Для реального приложения стоит добавить шифрование паролей и более сложную логику аутентификации.
Для запуска обоих сервисов вам нужно открыть два терминала и выполнить следующие команды:
Bash | 1
2
3
4
5
| # Терминал 1 - для API Gateway
nx serve api-gateway
# Терминал 2 - для Auth Microservice
nx serve auth-microservice |
|
Теперь давайте протестируем наше приложение. Сначала попробуем зарегистрировать нового пользователя:
Bash | 1
2
3
| curl -X POST http://localhost:3000/auth/signup \
-H "Content-Type: application/json" \
-d '{"username": "alice", "password": "secret"}' |
|
Если все настроено правильно, вы получите ответ с данными созданного пользователя. Теперь проверим вход в систему:
Bash | 1
2
3
| curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "alice", "password": "secret"}' |
|
Вы должны получить успешный ответ с данными пользователя. Если ввести неправильный пароль или несуществующего пользователя, API вернет ошибку.
Стоит отметить особенность работы ClientsModule — он подключается к микросервисам при первом использовании. Это означает, что первый запрос может занять больше времени, поскольку включает в себя установку соединения.
В реальных проектах часто используется паттерн "Публикация/Подписка" для асинхронной коммуникации. Для этого в NestJS есть метод emit() :
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
| // Отправка события
this.authClient.emit<void, UserCreatedEvent>('user_created', {
userId: user.id,
username: user.username
});
// Обработка события в другом сервисе
@EventPattern('user_created')
handleUserCreated(data: UserCreatedEvent) {
// Обработка события
} |
|
Этот паттерн отлично подходит для ситуаций, когда не требуется ответ от получателя, например, для отправки уведомлений или обновления кэша.
Для улучшения производительности и надежности системы можно настроить пул соединений в микросервисах. Это позволит обрабатывать больше запросов одновременно:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
| ClientsModule.register([
{
name: 'AUTH_MICROSERVICE',
transport: Transport.TCP,
options: {
host: 'localhost',
port: 3001,
retryAttempts: 5, // Количество попыток переподключения
retryDelay: 1000, // Задержка между попытками (мс)
},
},
]) |
|
Для обеспечения типобезопасности при коммуникации между сервисами рекомендуется создавать интерфейсы или типы для всех сообщений:
TypeScript | 1
2
3
4
5
6
| // shared/src/lib/interfaces/message-patterns.interface.ts
export interface AuthMessagePatterns {
'get_user': { username: string; password: string };
'create_user': { username: string; password: string };
'validate_token': { token: string };
} |
|
Это позволит TypeScript проверять типы аргументов при отправке сообщений и предотвращать ошибки на этапе компиляции.
При работе с микросервисами иногда возникают ситуации, когда сервис временно недоступен. Для обработки таких случаев можно использовать библиотеку RxJS, которая предоставляет операторы для повторных попыток и обработки ошибок:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
| import { catchError, timeout, retry } from 'rxjs/operators';
this.authService.getUser(createUserDto).pipe(
retry(3), // Повторить запрос до 3 раз в случае ошибки
timeout(5000), // Установить таймаут в 5 секунд
catchError(error => {
console.error('Ошибка при обращении к микросервису:', error);
// Вернуть запасной вариант или перебросить ошибку
throw new ServiceUnavailableException('Сервис аутентификации временно недоступен');
})
) |
|
Такой подход повышает отказоустойчивость системы и улучшает пользовательский опыт даже при возникновении проблем с отдельными сервисами.
Продвинутые техники
Когда ваша микросервисная архитектура растет и усложняется, возникает необходимость внедрения более продвинутых техник для обеспечения надежности, производительности и удобства разработки. Давайте рассмотрим ключевые аспекты, которые сделают вашу микросервисную систему на NestJS по-настоящему промышленного уровня.
Обработка ошибок в микросервисной архитектуре требует особого внимания, поскольку проблемы могут возникать на разных уровнях. При взаимодействии между сервисами ошибки могут быть связаны с сетью, таймаутами или внутренними сбоями. NestJS предлагает несколько механизмов для обработки таких ситуаций. Для централизованной обработки ошибок в микросервисах можно создать фильтры исключений:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| @Catch()
export class AllExceptionsFilter implements RpcExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToRpc();
const data = ctx.getData();
// Логируем ошибку вместе с контекстом
console.error(`Ошибка при обработке ${JSON.stringify(data)}:`, exception);
// Трансформируем ошибку в понятный формат
return throwError(() => ({
status: 'error',
message: 'Произошла внутренняя ошибка',
code: 'INTERNAL_ERROR',
timestamp: new Date().toISOString(),
}));
}
} |
|
В API Gateway важно корректно обрабатывать и трансформировать ошибки от микросервисов:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| @Post('login')
async login(@Body() createUserDto: CreateUserDto) {
try {
const user = await lastValueFrom(
this.authService.getUser(createUserDto).pipe(
timeout(5000),
catchError(err => {
if (err instanceof TimeoutError) {
throw new ServiceUnavailableException('Сервис аутентификации не отвечает');
}
// Обрабатываем ошибки от микросервисов
if (err?.status === 'error') {
throw new InternalServerErrorException(err.message);
}
throw err;
})
)
);
// Остальной код...
} catch (error) {
// Обработка других ошибок
throw error;
}
} |
|
Мониторинг и логирование играют ключевую роль в поддержании работоспособности микросервисной архитектуры. Без достаточной видимости происходящего в системе становится практически невозможно диагностировать и исправлять проблемы.
NestJS предлагает встроенный механизм логирования, но для микросервисов стоит рассмотреть более продвинутые решения. Централизованное логирование с использованием ELK-стека (Elasticsearch, Logstash, Kibana) или стека Grafana Loki становится необходимостью:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| // logger.service.ts
@Injectable()
export class LoggerService implements LoggerService {
private context?: string;
private correlationId?: string;
setContext(context: string) {
this.context = context;
return this;
}
setCorrelationId(id: string) {
this.correlationId = id;
return this;
}
log(message: string, ...args: any[]) {
this.writeLog('log', message, ...args);
}
error(message: string, trace?: string, ...args: any[]) {
this.writeLog('error', message, ...(trace ? [trace] : []), ...args);
}
private writeLog(level: string, message: string, ...args: any[]) {
const logEntry = {
level,
message,
context: this.context,
correlationId: this.correlationId,
timestamp: new Date().toISOString(),
// Добавляем метаданные, полезные для диагностики
hostname: os.hostname(),
pid: process.pid,
data: args.length ? args : undefined
};
console.log(JSON.stringify(logEntry));
// В реальном приложении отправляем логи в центральное хранилище
}
} |
|
Для эффективной трассировки запросов, проходящих через несколько микросервисов, используйте correlation ID — уникальный идентификатор, который передается вместе с запросом:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| @Injectable()
export class CorrelationIdInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
let correlationId: string;
if (context.getType() === 'http') {
const request = context.switchToHttp().getRequest();
correlationId = request.headers['x-correlation-id'] || uuidv4();
// Сохраняем ID в запросе для дальнейшего использования
request.correlationId = correlationId;
} else if (context.getType() === 'rpc') {
const rpcContext = context.switchToRpc();
const data = rpcContext.getData();
correlationId = data.correlationId || uuidv4();
}
// При отправке запросов в другие микросервисы добавляйте correlationId
// в метаданные или тело запроса
return next.handle().pipe(
tap(data => {
// Можно добавить correlationId в ответ
if (context.getType() === 'http') {
const response = context.switchToHttp().getResponse();
response.header('x-correlation-id', correlationId);
}
})
);
}
} |
|
Для масштабирования микросервисов существует несколько стратегий. Горизонтальное масштабирование предполагает запуск нескольких экземпляров одного сервиса. NestJS поддерживает такой подход из коробки — клиентский модуль автоматически распределяет нагрузку между экземплярами, если они работают с одним транспортом.
Для более сложных сценариев масштабирования можно использовать паттерн "Load Balanced Proxy":
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| @Module({
imports: [
ClientsModule.registerAsync([{
name: 'AUTH_SERVICE',
useFactory: (configService: ConfigService) => ({
transport: Transport.TCP,
options: {
host: configService.get('AUTH_SERVICE_HOST'),
port: configService.get('AUTH_SERVICE_PORT'),
},
}),
inject: [ConfigService],
}]),
],
})
export class AuthModule {} |
|
Здесь мы используем ConfigService для получения настроек из конфигурации, что позволяет легко переключаться между разными средами и инфраструктурой.
Тестирование микросервисной архитектуры требует многоуровневого подхода. Для модульных тестов отдельных сервисов можно использовать стандартные инструменты NestJS:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| describe('AuthService', () => {
let service: AuthService;
let clientProxyMock: any;
beforeEach(async () => {
clientProxyMock = {
send: jest.fn().mockImplementation(() => {
return of({ id: 1, username: 'test' });
}),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
{
provide: 'AUTH_MICROSERVICE',
useValue: clientProxyMock,
},
],
}).compile();
service = module.get<AuthService>(AuthService);
});
it('должен получать пользователя по имени', async () => {
const result = await lastValueFrom(service.getUser({ username: 'test', password: 'pass' }));
expect(result).toEqual({ id: 1, username: 'test' });
expect(clientProxyMock.send).toHaveBeenCalledWith('get_user', { username: 'test', password: 'pass' });
});
}); |
|
Для интеграционных тестов микросервисов можно запускать тестовые экземпляры и проверять их взаимодействие:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| describe('Интеграционные тесты аутентификации', () => {
let app: INestApplication;
let authMicroservice: INestMicroservice;
beforeAll(async () => {
// Запускаем микросервис аутентификации
const authModule = await NestFactory.createMicroservice<MicroserviceOptions>(
AuthModule,
{
transport: Transport.TCP,
options: {
host: 'localhost',
port: 3001,
},
},
);
await authMicroservice.listen();
// Запускаем API Gateway
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
await authMicroservice.close();
});
it('должен регистрировать нового пользователя', async () => {
const response = await request(app.getHttpServer())
.post('/auth/signup')
.send({ username: 'testuser', password: 'password' })
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.username).toBe('testuser');
});
}); |
|
Для обработки распределенных транзакций в микросервисной архитектуре часто применяется паттерн Saga. Он позволяет координировать несколько операций, которые должны быть выполнены как единое целое, с возможностью отката при сбое:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| @Injectable()
export class OrderSagaService {
constructor(
private readonly productClient: ClientProxy,
private readonly paymentClient: ClientProxy,
private readonly shippingClient: ClientProxy,
private readonly eventBus: EventBus,
) {}
async createOrder(orderData: CreateOrderDto): Promise<Order> {
// Шаг 1: Создаем заказ
const order = await this.saveOrder(orderData);
try {
// Шаг 2: Резервируем товары
await lastValueFrom(this.productClient.send('reserve_products', {
orderId: order.id,
items: orderData.items
}));
// Шаг 3: Обрабатываем платеж
await lastValueFrom(this.paymentClient.send('process_payment', {
orderId: order.id,
amount: orderData.totalAmount,
paymentDetails: orderData.paymentDetails
}));
// Шаг 4: Организуем доставку
await lastValueFrom(this.shippingClient.send('arrange_shipping', {
orderId: order.id,
address: orderData.shippingAddress
}));
// Отмечаем заказ как успешно созданный
await this.finalizeOrder(order.id);
return order;
} catch (error) {
// В случае ошибки запускаем компенсирующие действия
this.eventBus.publish(new OrderFailedEvent(order.id, error));
throw new InternalServerErrorException('Не удалось создать заказ');
}
}
// Компенсирующие действия в случае сбоя
@EventPattern('order.failed')
async handleOrderFailed(data: { orderId: string, step: string }) {
switch(data.step) {
case 'payment':
// Освобождаем зарезервированные товары
await lastValueFrom(this.productClient.send('release_products', { orderId: data.orderId }));
break;
case 'shipping':
// Отменяем платеж и освобождаем товары
await lastValueFrom(this.paymentClient.send('refund_payment', { orderId: data.orderId }));
await lastValueFrom(this.productClient.send('release_products', { orderId: data.orderId }));
break;
}
// Отмечаем заказ как неудачный
await this.cancelOrder(data.orderId);
}
} |
|
В этом примере мы используем событийно-ориентированный подход для организации распределенной транзакции. Если какой-то шаг завершается неудачно, мы публикуем событие, которое запускает соответствующие компенсирующие действия.
Миграция и заключение
Переход от монолита к микросервисам — процесс, требующий продуманного подхода. Распространённая ошибка — попытаться провести миграцию за один большой шаг, что часто приводит к катастрофическим последствиям. Вместо этого я рекомендую применять стратегию постепенной трансформации — выделять одну функциональность за другой. При миграции существующего приложения первым этапом становится анализ его структуры и выявление границ ответственности. Ищите в монолите отдельные модули, которые минимально связаны с остальной частью системы — они лучшие кандидаты на превращение в микросервисы.
Подход "странглера" (Strangler Pattern) прекрасно работает в таких сценариях: новые микросервисы постепенно обволакивают старый монолит, забирая его функциональность шаг за шагом, пока в конечном итоге он полностью не заменяется. Этот метод позволяет вашей системе оставаться работоспособной на протяжении всего процесса трансформации.
На практике при миграции приходится решать некоторые типичные проблемы. Одна из них — организация общего доступа к данным. В монолите все компоненты используют единую базу данных, а в микросервисной архитектуре каждый сервис должен иметь собственное хранилище. Это требует пересмотра доступа к данным и может включать дублирование некоторой информации между сервисами.
При переходе на микросервисную архитектуру учитывайте, что сложность системы смещается с кодовой базы на инфраструктуру и коммуникации между сервисами. Заранее планируйте мониторинг, логирование, трассировку — они становятся критически важными элементами.
Микросервисы на основе NestJS, TCP и TypeScript предоставляют мощную комбинацию для создания современных масштабируемых систем. NestJS структурирует код и обеспечивает модульность, TypeScript добавляет типовую безопасность, а TCP предлагает эффективную коммуникацию между сервисами. Вместе с преимуществами помните и о сложностях: увеличиваются требования к инфраструктуре, появляются новые точки отказа, усложняется развёртывание. Используйте инструменты оркестрации вроде Kubernetes для управления этой сложностью.
Начинайте с малого — создайте минимальный жизнеспособный микросервис, интегрируйте его с основной системой и постепенно наращивайте функциональность. Такой итеративный подход снижает риски и позволяет получать обратную связь на ранних этапах разработки.
Подключение модулей nestjs Здравствуйте. Подскажите пожалуйста, как исправить ошибки?
Error:(1, 22) TS2307: Cannot find module '@nestjs/common' or its corresponding type... nestjs postres docker failed 1) Запускаю локально
nest start
вываливается такая ошибка
8388 - 15.01.2022, 00:08:10 Unable to connect to the database.... Проектирование на NestJS, ошибки в настройках и тп Здравствуйте! Метод await this.audioQueue.add(*args), возвращающий экземпляр задания зависает в бесконечном ожидании.
В модуле app.service.ts, в... Микросервисы Добрый день, уважаемые коллеги!
Что подразумевает под собой понятие: «глубокое знание микросервисной архитектуры»?
Поделитесь своим мнением... NestJS редирект с одного сервера на другой Доброго времени суток, есть 2 сервера на NestJS, первый выступает в роли некого балансировщика, клиент отправляет запрос на некий эндпоинт и передаёт... Микросервисы и .NET Добрый вечер!
Кто применял в своей практике .NET микросервисы ASP.NET? Стоит ли связываться? Есть ли заметный выигрыш хоть в чём-то? Как... Микросервисы. Основы Добрый день, посоветуйте материал для построение микросервисов на c#. Желательно хотя одну ссылку на русском. Технологии ,которые используются при... Микросервисы (авторизация) Всем привет! Возник такой вопрос, Имеется "монолитной приложение" asp net core + react/redux, возникла идея скорее для общей практики, разбить... Микросервисы на Spring Boot Всем привет. Написал два микросервиса на Spring Boot, засекьюрил один обычным вводом пароля и мыла. Но проблема теперь в доступе с одного... Сцена зависает при запуске TCP-клиента, когда он подключен к TCP - серверу, при этом TCP-клиент полностью функционирует Проблема описана в заголовке, и хотелось бы услышать ваше мнение, о том как можно решить проблему.
Скрипт TCP-клиента на сцене:
using... Научить микросервисы передавать запросы друг другу Technologies:
· Spring core, MVC. (Don’t use Spring Boot)
· Hibernate
Task: develop 3 microservices using Spring MVC (as a servlet... SpringCloud ConfigServer и Микросервисы - ожидание запуска сервера конфигураций Всем доброго дня!
Подскажите куда копнуть, есть SpringCloudConfigServer - для распространения конфигураций микросервисов.
И есть парочка...
|