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

Архитектура

Приложение разделено на три микросервиса:

  • User Service – регистрация, авторизация и получение данных пользователей.
  • Chat Service – обработка сообщений и хранение истории чатов.
  • API Gateway – единая точка входа для маршрутизации запросов к нужным сервисам.

Сосредоточимся на нинициализации User Service. Он будет использовать:

  • JWT (с NestJS Passport) для авторизации.
  • Prisma для работы с PostgreSQL.
  • DTO с валидацией.
  • Swagger для генерации документации API.

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


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

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

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

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

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

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

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

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

npm install @nestjs/passport passport passport-jwt jsonwebtoken
npm install prisma @prisma/client --save-dev
npm install class-validator class-transformer
npm install @nestjs/swagger swagger-ui-express

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

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

npx prisma init

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

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

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

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  password  String
  name      String?
  createdAt DateTime @default(now())
}

Создайте миграцию и примените её (перед этим нужно будет поднять docker контейнер):

npx prisma migrate dev --name init

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

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

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

DTO для регистрации

Создайте файл src/auth/dto/register.dto.ts:

import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, MinLength, Matches } from 'class-validator';

export class RegisterDto {
  @ApiProperty({ example: 'user@example.com' })
  @IsEmail()
  email: string;

  @ApiProperty({
    example: 'StrongPass1',
    description: 'Пароль должен состоять минимум из 8 символов и содержать хотя бы одну заглавную букву',
  })
  @IsNotEmpty()
  @MinLength(8)
  @Matches(/^(?=.*[A-Z]).{8,}$/, {
    message:
      'Пароль должен состоять минимум из 8 символов и содержать хотя бы одну заглавную букву',
  })
  password: string;

  @ApiProperty({ example: 'John Doe', required: false })
  name?: string;
}

DTO для логина

Создайте файл src/auth/dto/login.dto.ts:

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

export class LoginDto {
  @ApiProperty({ example: 'user@example.com' })
  @IsEmail()
  email: string;

  @ApiProperty({ example: 'StrongPass1' })
  @IsNotEmpty()
  password: string;
}

Создание сервиса и контроллера пользователей

Пользовательский сервис

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

nest generate module users
nest generate controller users
nest generate service users

В src/users/users.service.ts реализуйте методы регистрации и валидации пользователя:

import { Injectable, ConflictException, UnauthorizedException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { RegisterDto } from '../auth/dto/register.dto';
import * as bcrypt from 'bcrypt';

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

  async register(dto: RegisterDto) {
    const hash = await bcrypt.hash(dto.password, 10);
    try {
      const user = await this.prisma.user.create({
        data: { email: dto.email, password: hash, name: dto.name },
      });
      return user;
    } catch (error) {
      throw new ConflictException('Пользователь с таким email уже существует');
    }
  }

  async validateUser(email: string, password: string) {
    const user = await this.prisma.user.findUnique({ where: { email } });
    if (user && await bcrypt.compare(password, user.password)) {
      const { password, ...result } = user;
      return result;
    }
    throw new UnauthorizedException('Неверные учетные данные');
  }

  async findById(id: number) {
    return this.prisma.user.findUnique({ where: { id } });
  }
}

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();
    });
  }
}

Не забудьте создать и зарегистрировать модуль для Prisma (например, src/prisma/prisma.module.ts), который экспортирует PrismaService.

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

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

Реализация JWT аутентификации с использованием Passport

Модуль аутентификации

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

nest generate module auth
nest generate service auth
nest generate controller auth

В src/auth/auth.service.ts реализуйте методы аутентификации и генерации JWT:

import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
import { LoginDto } from './dto/login.dto';

@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService,
  ) {}

  async validateUser(email: string, pass: string) {
    return this.usersService.validateUser(email, pass);
  }

  async login(loginDto: LoginDto) {
    const user = await this.validateUser(loginDto.email, loginDto.password);
    const payload = { email: user.email, sub: user.id };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}

Создайте стратегию JWT в файле src/auth/jwt.strategy.ts:

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_SECRET || 'defaultSecretKey',
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, email: payload.email };
  }
}

В src/auth/auth.module.ts подключите необходимые модули:

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: process.env.JWT_SECRET || 'defaultSecretKey',
      signOptions: { expiresIn: '1d' },
    }),
  ],
  providers: [AuthService, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

Контроллер аутентификации

Создайте src/auth/auth.controller.ts:

import { Body, Controller, Post, UsePipes, ValidationPipe } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { RegisterDto } from './dto/register.dto';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { ApiTags } from '@nestjs/swagger';

@ApiTags('auth')
@Controller('auth')
export class AuthController {
  constructor(
    private usersService: UsersService,
    private authService: AuthService,
  ) {}

  @Post('register')
  @UsePipes(new ValidationPipe())
  async register(@Body() dto: RegisterDto) {
    return this.usersService.register(dto);
  }

  @Post('login')
  @UsePipes(new ValidationPipe())
  async login(@Body() dto: LoginDto) {
    return this.authService.login(dto);
  }
}

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

В файле src/main.ts настройте Swagger:

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('User Service API')
    .setDescription('Документация API для управления пользователями')
    .setVersion('1.0')
    .addBearerAuth()
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api-docs', app, document);

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

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


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

В корне проекта user-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

# Собираем проект
RUN npm run build

# Открываем порт
EXPOSE 3001

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

Вариант с одновременным запуском всех сервисов:

  1. Создаем bash скрипт:
#!/bin/sh

set -e

echo "Waiting for database to be ready..."
until npx prisma migrate deploy > /dev/null 2>&1; do
  echo "Database not ready yet. Retrying in 3 seconds..."
  sleep 3
done

echo "Applying database migrations..."
npx prisma migrate deploy

echo "Starting application..."
exec node dist/src/main.js
  1. Меняем Dockerfile
FROM node:20-alpine as builder

RUN apk add --no-cache openssl \
    && apk add --no-cache curl

ENV NODE_ENV build

USER node
WORKDIR /home/node

COPY package*.json ./
RUN npm ci

COPY --chown=node:node . .
RUN npx prisma generate \
    && npm run build \
    && npm prune --omit=dev

# ---

FROM node:20-alpine as production

RUN apk add --no-cache openssl \
    && apk add --no-cache curl

ENV NODE_ENV production

USER node
WORKDIR /home/node

COPY --from=builder --chown=node:node /home/node/package*.json ./
COPY --from=builder --chown=node:node /home/node/node_modules/ ./node_modules/
COPY --from=builder --chown=node:node /home/node/dist/ ./dist/
COPY --from=builder --chown=node:node /home/node/prisma/ ./prisma/
COPY --from=builder --chown=node:node /home/node/entrypoint.sh ./entrypoint.sh

RUN chmod +x entrypoint.sh

ENTRYPOINT ["./entrypoint.sh"]

docker‑compose и .env файл

.env файл

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

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

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, api-gateway) добавляем по аналогичной схеме

volumes:
  postgres_data:

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