Создание и настройка Chat-Service

Архитектура

Chat Service предназначен для приема, обработки и рассылки сообщений. Он реализует двунаправленный протокол WebSocket для обмена данными между клиентом и сервером, что обеспечивает низкую задержку и возможность «живого» общения.

WebSockets – это протокол, который позволяет устанавливать постоянное соединение между клиентом и сервером посредством одноразового рукопожатия, после которого происходит двунаправленная передача данных в реальном времени. Благодаря этому протоколу можно передавать данные без необходимости повторного установления соединения для каждого запроса. (Подробнее см. WebSocket Overview.)

В данном руководстве Chat Service будет использовать:

  • WebSockets (с NestJS) для обработки сообщений в реальном времени.
  • Prisma для работы с PostgreSQL.
  • DTO с валидацией для структурирования данных.
  • Swagger для генерации документации API (при наличии HTTP‑эндпоинтов, например, для получения истории чатов).

Также будет создан отдельный Dockerfile для микросервиса и дополнен docker‑compose файл для всего проекта, который поднимает PostgreSQL (образ: postgres:17-alpine) с использованием переменных окружения из файла .env.


2. Установка зависимостей и подготовка проекта

Инструменты и зависимости

Убедитесь, что установлены:

  • Node.js, npm, NestJS CLI
  • Docker и docker‑compose

Создание проекта микросервиса

Создаём директорию для сервиса и инициализируем новый проект NestJS:

mkdir chat-service && cd chat-service
nest new .

При помощи пакетного менеджера npm установите дополнительные зависимости:

npm install @nestjs/websockets @nestjs/platform-socket.io
npm install prisma @prisma/client --save-dev
npm install class-validator class-transformer
npm install @nestjs/swagger swagger-ui-express

Дополнительные зависимости необходимы для реализации функционала WebSocket (пакеты @nestjs/websockets и @nestjs/platform-socket.io), работы с базой данных PostgreSQL через Prisma, валидации данных в DTO, и для генерации документации с помощью Swagger.


Настройка Prisma и базы данных PostgreSQL

Инициализируйте Prisma:

npx prisma init

В файле prisma/schema.prisma задайте следующую конфигурацию:

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Message {
  id        Int      @id @default(autoincrement())
  content   String
  senderId  Int
  room      String?   // Название комнаты или канала, если потребуется
  createdAt DateTime @default(now())
}

Модель Message хранит сообщения чата, включая содержание, идентификатор отправителя, название комнаты (если используется) и время создания. Автоматическая генерация идентификатора (autoincrement) и текущей даты (now()) упрощают ведение истории чатов.

Создайте миграцию и примените её (перед этим убедитесь, что docker-контейнер с PostgreSQL запущен):

npx prisma migrate dev --name init

В файле .env (на уровне проекта) добавьте строку подключения:

DATABASE_URL="postgresql://dbuser:dbpassword@localhost:5432/chat?schema=public"

Значение переменной DATABASE_URL может быть переопределено docker‑compose файлом для работы в контейнере, но оно будет использоваться для локальной разработки.


Создание DTO с валидацией и Swagger‑документацией

DTO для отправки сообщения

Создайте файл src/chat/dto/send-message.dto.ts:

import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, IsNumber } from 'class-validator';

export class SendMessageDto {
  @ApiProperty({ example: 'Привет, как дела?' })
  @IsNotEmpty({ message: 'Содержание сообщения не должно быть пустым' })
  @IsString({ message: 'Содержание сообщения должно быть строкой' })
  content: string;

  @ApiProperty({ example: 123, description: 'ID пользователя-отправителя' })
  @IsNotEmpty({ message: 'ID отправителя обязателен' })
  @IsNumber({}, { message: 'ID отправителя должен быть числом' })
  senderId: number;

  @ApiProperty({ example: 'general', description: 'Название комнаты или чата', required: false })
  room?: string;
}

Аннотации с помощью ApiProperty обеспечивают автоматическую генерацию документации Swagger, а валидаторы (например, IsNotEmpty и IsString) гарантируют корректность получаемых данных.

При необходимости можно создать дополнительные DTO для отображения истории сообщений или для формирования ответа API.


Создание сервиса и контроллера чатов

Prisma сервис

Создайте файл src/prisma/prisma.service.ts:

import { Injectable, OnModuleInit, INestApplication } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  async onModuleInit() {
    // Устанавливаем соединение с базой данных при инициализации модуля
    await this.$connect();
  }
  
  async enableShutdownHooks(app: INestApplication) {
    // Обеспечиваем корректное завершение работы приложения при остановке
    this.$on('beforeExit', async () => {
      await app.close();
    });
  }
}

PrismaService наследует возможности PrismaClient и добавляет методы для подключения к базе данных и корректного завершения работы приложения. Это упрощает повторное использование сервиса в различных модулях.

Создайте и зарегистрируйте модуль для Prisma, например, src/prisma/prisma.module.ts:

import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

Создание сервиса для работы с сообщениями

Создайте модуль, контроллер и сервис для чатов:

nest generate module chat
nest generate controller chat
nest generate service chat

В src/chat/chat.service.ts реализуйте методы для сохранения сообщений и получения истории чатов:

import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { SendMessageDto } from './dto/send-message.dto';

@Injectable()
export class ChatService {
  constructor(private prisma: PrismaService) {}

  /**
   * Сохраняет сообщение в базе данных.
   * @param dto DTO с информацией о сообщении.
   * @returns Сохраненное сообщение.
   */
  async saveMessage(dto: SendMessageDto) {
    const message = await this.prisma.message.create({
      data: {
        content: dto.content,
        senderId: dto.senderId,
        room: dto.room,
      },
    });
    return message;
  }

  /**
   * Получает историю сообщений. Если задан параметр room, возвращает сообщения из указанной комнаты.
   * @param room Название комнаты (опционально).
   * @returns Массив сообщений.
   */
  async getMessages(room?: string) {
    const query = room ? { where: { room } } : {};
    return this.prisma.message.findMany(query);
  }
}

Методы сервиса используют Prisma для выполнения операций над базой данных. Метод saveMessage сохраняет новое сообщение, а getMessages реализует поиск сообщений по критерию комнаты.

Реализация WebSocket Gateway для работы в реальном времени

Создайте файл src/chat/chat.gateway.ts:

import {
  WebSocketGateway,
  SubscribeMessage,
  MessageBody,
  WebSocketServer,
} from '@nestjs/websockets';
import { Server } from 'socket.io';
import { ChatService } from './chat.service';
import { SendMessageDto } from './dto/send-message.dto';

@WebSocketGateway({ cors: true }) // Разрешаем кросс-доменные запросы
export class ChatGateway {
  @WebSocketServer()
  server: Server;

  constructor(private chatService: ChatService) {}

  /**
   * Обработчик входящих сообщений через WebSocket.
   * При получении события 'sendMessage' сохраняет сообщение в базе и транслирует его всем подключенным клиентам.
   * @param data Данные сообщения, переданные клиентом.
   * @returns Сохраненное сообщение.
   */
  @SubscribeMessage('sendMessage')
  async handleSendMessage(@MessageBody() data: SendMessageDto) {
    // Сохраняем сообщение в базу данных
    const savedMessage = await this.chatService.saveMessage(data);

    // Трансляция сообщения всем подключенным клиентам
    this.server.emit('message', savedMessage);
    return savedMessage;
  }
}

Метод handleSendMessage является обработчиком события WebSocket.

"WebSockets позволяют устанавливать постоянное соединение, что дает возможность серверу и клиенту обмениваться сообщениями в режиме реального времени без необходимости повторного установления соединения. Это особенно важно для чатов, игровых приложений и онлайн-трансляций."

HTTP контроллер для работы с историей сообщений (опционально)

Если необходимо предоставлять историю сообщений через HTTP‑эндпоинты, настройте контроллер:

В src/chat/chat.controller.ts:

import { Controller, Get, Query } from '@nestjs/common';
import { ChatService } from './chat.service';
import { ApiTags, ApiQuery } from '@nestjs/swagger';

@ApiTags('chat')
@Controller('chat')
export class ChatController {
  constructor(private readonly chatService: ChatService) {}

  /**
   * HTTP GET метод для получения истории сообщений.
   * Если параметр room указан, возвращаются сообщения из конкретной комнаты.
   * @param room Название комнаты (опционально).
   * @returns Массив сообщений.
   */
  @Get('history')
  @ApiQuery({ name: 'room', required: false, description: 'Название комнаты чата' })
  async getHistory(@Query('room') room?: string) {
    return this.chatService.getMessages(room);
  }
}

Этот эндпоинт может использоваться для исторического просмотра сообщений, а также для отладки или аналитики чатов.

Обновление модуля Chat

В файле src/chat/chat.module.ts импортируйте необходимые модули и зарегистрируйте компоненты:

import { Module } from '@nestjs/common';
import { ChatService } from './chat.service';
import { ChatController } from './chat.controller';
import { ChatGateway } from './chat.gateway';
import { PrismaModule } from '../prisma/prisma.module';

@Module({
  imports: [PrismaModule],
  providers: [ChatService, ChatGateway],
  controllers: [ChatController],
})
export class ChatModule {}

Модуль объединяет в себе сервис, контроллер и gateway, что позволяет распределять ответственность за хранение, обработку и доставку сообщений.


Интеграция Swagger для автоматической документации

Если в Chat Service реализованы HTTP‑эндпоинты (например, для получения истории сообщений), настройте Swagger в файле src/main.ts:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());

  // Настройка Swagger
  const config = new DocumentBuilder()
    .setTitle('Chat Service API')
    .setDescription('Документация API для работы с сообщениями и историей чатов')
    .setVersion('1.0')
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api-docs', app, document);

  await app.listen(3002);
}
bootstrap();

После запуска приложения документация будет доступна по адресу:
http://localhost:3002/api-docs


Dockerfile для микросервиса

В корне проекта chat-service создайте файл Dockerfile:

# Используем официальный Node.js образ
FROM node:20-alpine

# Создаем рабочую директорию внутри контейнера
WORKDIR /usr/src/app

# Копируем файлы package.json и package-lock.json в рабочую директорию
COPY package*.json ./

# Устанавливаем зависимости
RUN npm install

# Копируем весь исходный код приложения в контейнер
COPY . .

# Генерируем Prisma клиент для работы с базой данных
RUN npx prisma generate

# Собираем проект (компиляция TypeScript в JavaScript)
RUN npm run build

# Открываем порт (например, 3002)
EXPOSE 3002

# Запускаем приложение
CMD ["node", "dist/main.js"]

В Dockerfile описан стандартный процесс сборки и запуска Node.js приложения: установка зависимостей, генерация Prisma клиента, сборка проекта и запуск конечного приложения.

Для продакшен-сборки можно применить технику многоступенчатой сборки, что поможет уменьшить размер конечного образа.

Вариант с использованием многоступенчатой сборки
# Stage 1: Сборка приложения
FROM node:20-alpine as builder

WORKDIR /home/node

COPY package*.json ./
RUN npm ci

COPY . .
RUN npx prisma generate && npm run build && npm prune --omit=dev

# Stage 2: Запуск в продакшене
FROM node:20-alpine as production

WORKDIR /home/node

COPY --from=builder /home/node/package*.json ./
COPY --from=builder /home/node/node_modules/ ./node_modules/
COPY --from=builder /home/node/dist/ ./dist/
COPY --from=builder /home/node/prisma/ ./prisma/

EXPOSE 3002
CMD ["node", "dist/main.js"]

docker‑compose и .env файл

.env файл

В корневой директории общего проекта (рядом с docker‑compose файлом) создайте файл .env со следующим содержимым:

DATABASE_USERNAME=dbuser
DATABASE_PASSWORD=dbpassword
DATABASE_NAME=chat
JWT_SECRET=YourJWTSecretKey

Все параметры подключения и секреты вынесены в отдельный файл .env, что упрощает управление конфигурацией в разных окружениях.

docker‑compose файл

Создайте файл docker-compose.yml:

version: '3.8'
services:
  postgres:
    image: postgres:17-alpine
    restart: always
    environment:
      POSTGRES_USER: ${DATABASE_USERNAME}
      POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
      POSTGRES_DB: ${DATABASE_NAME}
    ports:
      - '5432:5432'
    volumes:
      - postgres_data:/var/lib/postgresql/data

  user-service:
    build: ./user-service
    restart: always
    ports:
      - '3001:3001'
    environment:
      DATABASE_URL: "postgresql://${DATABASE_USERNAME}:${DATABASE_PASSWORD}@postgres:5432/${DATABASE_NAME}?schema=public"
      JWT_SECRET: ${JWT_SECRET}
    depends_on:
      - postgres

  chat-service:
    build: ./chat-service
    restart: always
    ports:
      - '3002:3002'
    environment:
      DATABASE_URL: "postgresql://${DATABASE_USERNAME}:${DATABASE_PASSWORD}@postgres:5432/${DATABASE_NAME}?schema=public"
    depends_on:
      - postgres

  # API Gateway можно добавить по аналогичной схеме

volumes:
  postgres_data:

docker‑compose файл объединяет все сервисы (PostgreSQL, User Service, Chat Service). Благодаря использованию файла .env переменные удобно переопределяются, что упрощает разворачивание проекта как в локальной среде, так и в продакшене.