Создание и настройка 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 переменные удобно переопределяются, что упрощает разворачивание проекта как в локальной среде, так и в продакшене.