32 KiB
32 KiB
Рекомендации по миграции бэкенда на Node.js (Express + TypeORM)
Обзор текущей архитектуры
Текущий стек технологий
- Backend: FastAPI 0.104.1 (Python 3.11)
- База данных: PostgreSQL 15 + SQLAlchemy 2.0 + Alembic
- Аутентификация: JWT (access/refresh токены) + bcrypt
- Frontend: Vanilla JavaScript SPA с динамической загрузкой модулей
- Инфраструктура: Docker Compose, Nginx, Alpine Linux
- Документация API: Автоматическая через FastAPI (Swagger/ReDoc)
Структура бэкенда
backend/
├── app/
│ ├── __init__.py
│ ├── main.py # Точка входа, настройка FastAPI
│ ├── auth.py # JWT аутентификация, хеширование паролей
│ ├── database.py # Подключение к БД, сессии SQLAlchemy
│ ├── models.py # SQLAlchemy ORM модели (4 сущности)
│ ├── schemas.py # Pydantic схемы валидации (11 классов)
│ ├── crud.py # CRUD операции (135 строк бизнес-логики)
│ ├── middleware/
│ │ └── cors.py # CORS настройки
│ └── routes/
│ ├── auth.py # Аутентификация (регистрация, вход, выход, me)
│ ├── courses.py # Курсы (список, детали)
│ ├── favorites.py # Избранные курсы (получение, добавление, удаление)
│ ├── progress.py # Прогресс обучения (CRUD)
│ └── stats.py # Статистика системы
├── alembic/ # Миграции базы данных
├── init.sql # Инициализация БД с тестовыми данными
├── requirements.txt # Python зависимости (13 пакетов)
└── Dockerfile # Docker образ Python 3.11
Ключевые сущности данных
- User (
users): пользователи системы (email, хеш пароля, активность) - Course (
courses): обучающие курсы (название, описание, длительность, уровень) - FavoriteCourse (
favorite_courses): связь многие-ко-многим пользователей и курсов (избранное) - Progress (
progress): прогресс обучения по курсам (пройдено уроков, всего уроков)
API Endpoints
| Метод | Путь | Описание | Аутентификация |
|---|---|---|---|
| POST | /api/auth/register |
Регистрация пользователя | Нет |
| POST | /api/auth/login |
Вход, получение JWT токенов | Нет |
| POST | /api/auth/logout |
Выход (символический) | Да |
| GET | /api/auth/me |
Информация о текущем пользователе | Да |
| GET | /api/courses |
Список курсов с фильтрацией по уровню | Нет |
| GET | /api/courses/{id} |
Детали курса | Нет |
| GET | /api/favorites |
Избранные курсы пользователя | Да |
| POST | /api/favorites |
Добавить курс в избранное | Да |
| DELETE | /api/favorites/{id} |
Удалить курс из избранного | Да |
| GET | /api/progress |
Прогресс по всем курсам пользователя | Да |
| GET | /api/progress/{id} |
Прогресс по конкретному курсу | Да |
| POST | /api/progress/{id} |
Обновить прогресс по курсу | Да |
| DELETE | /api/progress/{id} |
Удалить прогресс по курсу | Да |
| GET | /api/stats |
Статистика системы (пользователи, курсы) | Нет |
Аутентификация и авторизация
- JWT токены: access token (30 мин) + refresh token (7 дней)
- Хранение токенов: фронтенд сохраняет access token в localStorage
- Передача токенов: заголовок
Authorization: Bearer <token> - Хеширование паролей: bcrypt через библиотеку passlib
- Защита эндпоинтов: зависимость
get_current_userв FastAPI
Инфраструктура
- Docker Compose: 3 сервиса (postgres, backend, frontend)
- Сеть: изолированная сеть
auth_network - База данных: персистентный volume
postgres_data - Проксирование: Nginx раздает статику и проксирует API запросы к бэкенду
- Hot reload: для разработки через
uvicorn --reload
Анализ для миграции на Node.js
Преимущества текущей архитектуры
- Чистая слоистая архитектура: модели, схемы, CRUD, маршруты
- Автоматическая валидация: Pydantic схемы с детальными правилами
- Автоматическая документация: Swagger UI через FastAPI
- Асинхронная обработка: поддержка async/await в FastAPI
- Типизация: Python type hints + Pydantic
Сложности миграции
- Потеря автоматической документации (Swagger) - потребуется Swagger UI для Express
- Потеря автоматической валидации - необходимо реализовать валидацию вручную
- Различия в ORM: SQLAlchemy vs TypeORM (разные подходы к отношениям, миграциям)
- Асинхронность: FastAPI использует настоящую асинхронность, Express - callback/promise
- Система зависимостей: FastAPI Dependency Injection vs Express middleware
Совместимость с фронтендом
- API интерфейс должен остаться неизменным для минимизации изменений фронтенда
- Структура ответов: все эндпоинты возвращают
SuccessResponseили массивы - Коды ошибок: HTTP статусы и структура
ErrorResponseдолжны сохраниться - Аутентификация: тот же механизм JWT с access token в localStorage
Рекомендуемый стек Node.js
Основные технологии
| Компонент | Технология | Обоснование |
|---|---|---|
| Фреймворк | Express 4.x | Минималистичный, близкий к текущей архитектуре, большое сообщество |
| ORM | TypeORM 0.3.x | TypeScript-ориентированный, поддержка PostgreSQL, миграции, ActiveRecord/DataMapper паттерны |
| Валидация | class-validator + class-transformer | Аналог Pydantic, декларативная валидация через декораторы |
| Аутентификация | jsonwebtoken + bcrypt | Стандартные библиотеки для JWT и хеширования паролей |
| Конфигурация | dotenv + config | Управление переменными окружения в разных средах |
| Документация API | swagger-jsdoc + swagger-ui-express | Автоматическая генерация Swagger документации |
| Миграции | TypeORM migrations | Встроенная система миграций TypeORM |
| Логирование | winston + morgan | Структурированное логирование + HTTP логгирование |
| Тестирование | Jest + Supertest | Unit и интеграционные тесты |
Альтернативы
- NestJS: более структурированный, но более сложный для миграции
- Prisma: современный ORM, но менее зрелый для сложных отношений
- Fastify: более производительный, но менее распространенный
Пошаговый план миграции
Этап 1: Подготовка и настройка окружения
- Создание нового бэкенда на Node.js в отдельной директории (например,
backend-node) - Инициализация проекта:
npm init, установка зависимостей - Настройка TypeScript: tsconfig, структура проекта
- Конфигурация Docker: создание Dockerfile для Node.js
- Обновление docker-compose.yml: замена сервиса backend
Этап 2: Настройка базы данных и TypeORM
- Определение сущностей TypeORM: преобразование SQLAlchemy моделей в TypeORM Entity
- Настройка подключения к БД: использование того же PostgreSQL
- Создание миграций: перенос init.sql в миграции TypeORM
- Тестирование подключения: проверка работы с существующей БД
Этап 3: Реализация базовой инфраструктуры
- Настройка Express: middleware (CORS, парсинг JSON, логирование)
- Реализация структуры ответов: SuccessResponse, ErrorResponse
- Настройка Swagger документации: описание всех эндпоинтов
- Создание системы конфигурации: переменные окружения
Этап 4: Реализация аутентификации
- Создание JWT сервиса: генерация, верификация токенов
- Реализация хеширования паролей: bcrypt
- Создание middleware аутентификации: аналог
get_current_user - Тестирование аутентификации: регистрация, вход, защита эндпоинтов
Этап 5: Реализация бизнес-логики
- Репозитории/сервисы: перенос CRUD операций из
crud.py - Контроллеры: реализация всех эндпоинтов из routes
- Валидация запросов: class-validator для DTO
- Обработка ошибок: централизованный обработчик ошибок
Этап 6: Интеграция и тестирование
- Поэтапная замена эндпоинтов: можно запустить оба бэкенда параллельно
- Тестирование фронтенда: проверка всех сценариев
- Нагрузочное тестирование: сравнение производительности
- Миграция данных: перенос существующих данных (если требуется)
Этап 7: Деплой и мониторинг
- Обновление Docker Compose: замена образа бэкенда
- Настройка мониторинга: логи, метрики, health checks
- Резервное копирование: стратегия бэкапов PostgreSQL
- Документация: обновление ARCHITECTURE.md
Структура проекта Node.js
backend-node/
├── src/
│ ├── index.ts # Точка входа
│ ├── app.ts # Настройка Express
│ ├── config/
│ │ ├── index.ts # Конфигурация приложения
│ │ ├── database.ts # Конфигурация БД
│ │ └── jwt.ts # Конфигурация JWT
│ ├── entities/ # TypeORM сущности
│ │ ├── User.ts
│ │ ├── Course.ts
│ │ ├── FavoriteCourse.ts
│ │ └── Progress.ts
│ ├── migrations/ # Миграции TypeORM
│ │ ├── 0001-initial-schema.ts
│ │ └── 0002-seed-data.ts
│ ├── middlewares/
│ │ ├── auth.middleware.ts # Аутентификация
│ │ ├── error.middleware.ts # Обработка ошибок
│ │ ├── validation.middleware.ts # Валидация DTO
│ │ └── cors.middleware.ts # CORS
│ ├── controllers/ # Контроллеры (маршруты)
│ │ ├── auth.controller.ts
│ │ ├── courses.controller.ts
│ │ ├── favorites.controller.ts
│ │ ├── progress.controller.ts
│ │ └── stats.controller.ts
│ ├── services/ # Бизнес-логика
│ │ ├── auth.service.ts
│ │ ├── courses.service.ts
│ │ ├── favorites.service.ts
│ │ ├── progress.service.ts
│ │ └── stats.service.ts
│ ├── repositories/ # Работа с БД (опционально)
│ │ ├── user.repository.ts
│ │ ├── course.repository.ts
│ │ └── base.repository.ts
│ ├── dtos/ # Data Transfer Objects
│ │ ├── auth.dto.ts
│ │ ├── courses.dto.ts
│ │ ├── favorites.dto.ts
│ │ ├── progress.dto.ts
│ │ └── common.dto.ts
│ ├── utils/
│ │ ├── api-response.ts # SuccessResponse, ErrorResponse
│ │ ├── logger.ts # Winston логгер
│ │ └── helpers.ts # Вспомогательные функции
│ └── types/ # TypeScript типы
│ └── index.ts
├── tests/ # Тесты
│ ├── unit/
│ └── integration/
├── docker/ # Docker конфигурация
│ └── Dockerfile
├── .env.example # Пример переменных окружения
├── package.json
├── tsconfig.json
├── ormconfig.ts # Конфигурация TypeORM
└── README.md
Примеры кода
Сущность User (TypeORM)
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, OneToMany } from 'typeorm';
import { FavoriteCourse } from './FavoriteCourse';
import { Progress } from './Progress';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: 'varchar', length: 255, unique: true })
email: string;
@Column({ type: 'varchar', length: 255 })
hashedPassword: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@Column({ type: 'boolean', default: true })
isActive: boolean;
@OneToMany(() => FavoriteCourse, favorite => favorite.user)
favoriteCourses: FavoriteCourse[];
@OneToMany(() => Progress, progress => progress.user)
progress: Progress[];
}
DTO с валидацией (class-validator)
import { IsEmail, IsString, MinLength, MaxLength, Matches } from 'class-validator';
export class UserCreateDto {
@IsEmail()
email: string;
@IsString()
@MinLength(8)
@MaxLength(100)
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, {
message: 'Password must contain at least one uppercase letter, one lowercase letter and one number'
})
password: string;
}
export class UserLoginDto {
@IsEmail()
email: string;
@IsString()
password: string;
}
Контроллер аутентификации
import { Request, Response, NextFunction } from 'express';
import { AuthService } from '../services/auth.service';
import { UserCreateDto, UserLoginDto } from '../dtos/auth.dto';
import { successResponse, errorResponse } from '../utils/api-response';
import { validate } from '../middlewares/validation.middleware';
export class AuthController {
private authService: AuthService;
constructor() {
this.authService = new AuthService();
}
register = [
validate(UserCreateDto),
async (req: Request, res: Response, next: NextFunction) => {
try {
const userData: UserCreateDto = req.body;
await this.authService.register(userData);
return successResponse(res, {
message: 'User registered successfully',
data: { email: userData.email }
});
} catch (error) {
next(error);
}
}
];
login = [
validate(UserLoginDto),
async (req: Request, res: Response, next: NextFunction) => {
try {
const credentials: UserLoginDto = req.body;
const result = await this.authService.login(credentials);
return successResponse(res, {
message: 'Login successful',
data: result
});
} catch (error) {
next(error);
}
}
];
getMe = [
// authMiddleware применяется в маршрутах
async (req: Request, res: Response, next: NextFunction) => {
try {
const user = (req as any).user; // После authMiddleware
return successResponse(res, {
data: {
email: user.email,
id: user.id,
createdAt: user.createdAt,
isActive: user.isActive
}
});
} catch (error) {
next(error);
}
}
];
}
Сервис аутентификации
import { Repository } from 'typeorm';
import { AppDataSource } from '../config/database';
import { User } from '../entities/User';
import { UserCreateDto, UserLoginDto } from '../dtos/auth.dto';
import { JwtService } from './jwt.service';
import { PasswordService } from './password.service';
import { BadRequestError, UnauthorizedError } from '../utils/errors';
export class AuthService {
private userRepository: Repository<User>;
private jwtService: JwtService;
private passwordService: PasswordService;
constructor() {
this.userRepository = AppDataSource.getRepository(User);
this.jwtService = new JwtService();
this.passwordService = new PasswordService();
}
async register(userData: UserCreateDto): Promise<void> {
// Проверка существования пользователя
const existingUser = await this.userRepository.findOne({
where: { email: userData.email }
});
if (existingUser) {
throw new BadRequestError('Email already registered');
}
// Хеширование пароля
const hashedPassword = await this.passwordService.hashPassword(userData.password);
// Создание пользователя
const user = this.userRepository.create({
email: userData.email,
hashedPassword
});
await this.userRepository.save(user);
}
async login(credentials: UserLoginDto): Promise<{ accessToken: string; refreshToken: string; user: any }> {
// Поиск пользователя
const user = await this.userRepository.findOne({
where: { email: credentials.email }
});
if (!user) {
throw new UnauthorizedError('Incorrect email or password');
}
// Проверка пароля
const isValidPassword = await this.passwordService.verifyPassword(
credentials.password,
user.hashedPassword
);
if (!isValidPassword) {
throw new UnauthorizedError('Incorrect email or password');
}
// Генерация токенов
const accessToken = this.jwtService.generateAccessToken(user.email);
const refreshToken = this.jwtService.generateRefreshToken(user.email);
return {
accessToken,
refreshToken,
user: {
email: user.email,
id: user.id,
createdAt: user.createdAt
}
};
}
}
Миграция данных
Стратегии миграции
- Постепенная миграция: запуск Node.js бэкенда параллельно с Python, переключение по эндпоинтам
- Полная замена: остановка Python бэкенда, запуск Node.js с переносом данных
- Канареечное развертывание: направление части трафика на Node.js бэкенд
Перенос существующих данных
- Схема БД остается той же, только ORM меняется
- Данные пользователей: пароли захешированы bcrypt - совместимы
- Курсы и связи: прямокопирование таблиц
- Миграционный скрипт: можно написать на TypeScript с использованием TypeORM
Пример миграционного скрипта
// migrate-data.ts
import { createConnection } from 'typeorm';
import { User, Course, FavoriteCourse, Progress } from './src/entities';
async function migrate() {
// Подключение к существующей БД
const connection = await createConnection({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'auth_user',
password: 'auth_password',
database: 'auth_learning',
entities: [User, Course, FavoriteCourse, Progress],
synchronize: false // Не создавать таблицы автоматически
});
// Проверка данных
const userCount = await connection.manager.count(User);
console.log(`Users in database: ${userCount}`);
// Данные уже существуют, можно закрыть соединение
await connection.close();
}
Инфраструктура
Dockerfile для Node.js
FROM node:18-alpine
WORKDIR /app
# Установка зависимостей
COPY package*.json ./
RUN npm ci --only=production
# Копирование исходного кода
COPY . .
# Сборка TypeScript
RUN npm run build
# Создание непривилегированного пользователя
RUN addgroup -g 1000 -S nodejs && \
adduser -S nodejs -u 1000 -G nodejs
USER nodejs
EXPOSE 3000
CMD ["node", "dist/index.js"]
Обновленный docker-compose.yml
version: '3.8'
services:
postgres:
# Остается без изменений
backend:
build: ./backend-node
container_name: auth_learning_backend_node
environment:
NODE_ENV: production
DATABASE_URL: postgresql://${POSTGRES_USER:-auth_user}:${POSTGRES_PASSWORD:-auth_password}@postgres:5432/${POSTGRES_DB:-auth_learning}
JWT_SECRET: ${JWT_SECRET:-your-jwt-secret-change-in-production}
ACCESS_TOKEN_EXPIRE_MINUTES: ${ACCESS_TOKEN_EXPIRE_MINUTES:-30}
REFRESH_TOKEN_EXPIRE_DAYS: ${REFRESH_TOKEN_EXPIRE_DAYS:-7}
ports:
- "3000:3000"
depends_on:
postgres:
condition: service_healthy
networks:
- auth_network
# Для разработки с hot-reload:
# command: npm run dev
frontend:
# Остается без изменений, но нужно обновить nginx.conf для проксирования на порт 3000
networks:
auth_network:
driver: bridge
volumes:
postgres_data:
Обновление nginx.conf
location /api/ {
proxy_pass http://backend:3000; # Изменен порт с 8000 на 3000
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
Тестирование
Стратегия тестирования
- Юнит-тесты: сервисы, утилиты (Jest)
- Интеграционные тесты: API эндпоинты (Supertest)
- E2E тесты: полный поток с фронтендом (Cypress/Playwright)
- Нагрузочное тестирование: сравнение производительности с FastAPI
Пример теста API
import request from 'supertest';
import { app } from '../src/app';
describe('Auth API', () => {
describe('POST /api/auth/register', () => {
it('should register a new user', async () => {
const response = await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
password: 'Test1234'
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data.email).toBe('test@example.com');
});
});
});
Риски и их минимизация
Технические риски
| Риск | Вероятность | Влияние | Стратегия минимизации |
|---|---|---|---|
| Несовместимость API | Низкая | Высокое | Точное копирование структур ответов, тестирование каждого эндпоинта |
| Производительность | Средняя | Среднее | Нагрузочное тестирование, оптимизация запросов к БД |
| Потеря данных | Низкая | Критическое | Резервное копирование БД перед миграцией, поэтапное переключение |
| Ошибки аутентификации | Средняя | Высокое | Тщательное тестирование JWT, проверка совместимости bcrypt хешей |
Организационные риски
- Время миграции: оценка 2-4 недели для одного разработчика
- Обучение команды: если команда не знает TypeScript/Node.js
- Поддержка двух кодовых баз: в период параллельной работы
Преимущества перехода на Node.js
Технические преимущества
- Единый язык: JavaScript/TypeScript на фронтенде и бэкенде
- Большая экосистема: npm с огромным количеством пакетов
- Высокая производительность: Node.js хорошо справляется с I/O операциями
- Меньшее потребление памяти: по сравнению с Python + FastAPI
Бизнес-преимущества
- Упрощение найма: больше разработчиков знают JavaScript чем Python
- Более быстрое развитие: возможность переиспользования кода между фронтендом и бэкендом
- Лучшая масштабируемость: Node.js лучше подходит для микросервисной архитектуры
- Снижение затрат на инфраструктуру: более эффективное использование ресурсов
Рекомендации по внедрению
Приоритетность задач
-
Высокий приоритет:
- Настройка базовой инфраструктуры (Express, TypeORM, Docker)
- Реализация аутентификации (JWT, bcrypt)
- Миграция основных сущности (User, Course)
-
Средний приоритет:
- Реализация оставшихся эндпоинтов
- Настройка Swagger документации
- Интеграционное тестирование
-
Низкий приоритет:
- Оптимизация производительности
- Расширенные функции (пагинация, сортировка)
- Мониторинг и логирование
Контрольные точки
- Неделя 1: Базовая инфраструктура, подключение к БД, простые CRUD операции
- Неделя 2: Аутентификация, защита эндпоинтов, основные маршруты
- Неделя 3: Тестирование, интеграция с фронтендом, документация
- Неделя 4: Постепенное переключение, мониторинг, оптимизация
Критерии успеха
- Все существующие API эндпоинты работают без изменений фронтенда
- Производительность не хуже FastAPI версии
- 100% покрытие тестами критической функциональности
- Полная документация API и архитектуры
Заключение
Миграция бэкенда с FastAPI на Express + TypeORM является технически выполнимой задачей с четкими преимуществами для долгосрочного развития проекта. Рекомендуемый подход - поэтапная миграция с сохранением обратной совместимости API.
Ключевые факторы успеха:
- Сохранение API интерфейса для минимизации изменений фронтенда
- Тщательное тестирование каждого эндпоинта
- Поэтапное развертывание с возможностью отката
- Документирование всех изменений и решений
Предложенный стек (Express + TypeORM) обеспечивает хороший баланс между производительностью, поддерживаемостью и скоростью разработки, делая его подходящим выбором для данного проекта.