Files
pyisu/arch-node-recommendations.md
2026-03-13 14:39:43 +08:00

32 KiB
Raw Blame History

Рекомендации по миграции бэкенда на 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

Ключевые сущности данных

  1. User (users): пользователи системы (email, хеш пароля, активность)
  2. Course (courses): обучающие курсы (название, описание, длительность, уровень)
  3. FavoriteCourse (favorite_courses): связь многие-ко-многим пользователей и курсов (избранное)
  4. 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

Преимущества текущей архитектуры

  1. Чистая слоистая архитектура: модели, схемы, CRUD, маршруты
  2. Автоматическая валидация: Pydantic схемы с детальными правилами
  3. Автоматическая документация: Swagger UI через FastAPI
  4. Асинхронная обработка: поддержка async/await в FastAPI
  5. Типизация: Python type hints + Pydantic

Сложности миграции

  1. Потеря автоматической документации (Swagger) - потребуется Swagger UI для Express
  2. Потеря автоматической валидации - необходимо реализовать валидацию вручную
  3. Различия в ORM: SQLAlchemy vs TypeORM (разные подходы к отношениям, миграциям)
  4. Асинхронность: FastAPI использует настоящую асинхронность, Express - callback/promise
  5. Система зависимостей: 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: Подготовка и настройка окружения

  1. Создание нового бэкенда на Node.js в отдельной директории (например, backend-node)
  2. Инициализация проекта: npm init, установка зависимостей
  3. Настройка TypeScript: tsconfig, структура проекта
  4. Конфигурация Docker: создание Dockerfile для Node.js
  5. Обновление docker-compose.yml: замена сервиса backend

Этап 2: Настройка базы данных и TypeORM

  1. Определение сущностей TypeORM: преобразование SQLAlchemy моделей в TypeORM Entity
  2. Настройка подключения к БД: использование того же PostgreSQL
  3. Создание миграций: перенос init.sql в миграции TypeORM
  4. Тестирование подключения: проверка работы с существующей БД

Этап 3: Реализация базовой инфраструктуры

  1. Настройка Express: middleware (CORS, парсинг JSON, логирование)
  2. Реализация структуры ответов: SuccessResponse, ErrorResponse
  3. Настройка Swagger документации: описание всех эндпоинтов
  4. Создание системы конфигурации: переменные окружения

Этап 4: Реализация аутентификации

  1. Создание JWT сервиса: генерация, верификация токенов
  2. Реализация хеширования паролей: bcrypt
  3. Создание middleware аутентификации: аналог get_current_user
  4. Тестирование аутентификации: регистрация, вход, защита эндпоинтов

Этап 5: Реализация бизнес-логики

  1. Репозитории/сервисы: перенос CRUD операций из crud.py
  2. Контроллеры: реализация всех эндпоинтов из routes
  3. Валидация запросов: class-validator для DTO
  4. Обработка ошибок: централизованный обработчик ошибок

Этап 6: Интеграция и тестирование

  1. Поэтапная замена эндпоинтов: можно запустить оба бэкенда параллельно
  2. Тестирование фронтенда: проверка всех сценариев
  3. Нагрузочное тестирование: сравнение производительности
  4. Миграция данных: перенос существующих данных (если требуется)

Этап 7: Деплой и мониторинг

  1. Обновление Docker Compose: замена образа бэкенда
  2. Настройка мониторинга: логи, метрики, health checks
  3. Резервное копирование: стратегия бэкапов PostgreSQL
  4. Документация: обновление 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
            }
        };
    }
}

Миграция данных

Стратегии миграции

  1. Постепенная миграция: запуск Node.js бэкенда параллельно с Python, переключение по эндпоинтам
  2. Полная замена: остановка Python бэкенда, запуск Node.js с переносом данных
  3. Канареечное развертывание: направление части трафика на Node.js бэкенд

Перенос существующих данных

  1. Схема БД остается той же, только ORM меняется
  2. Данные пользователей: пароли захешированы bcrypt - совместимы
  3. Курсы и связи: прямокопирование таблиц
  4. Миграционный скрипт: можно написать на 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;
}

Тестирование

Стратегия тестирования

  1. Юнит-тесты: сервисы, утилиты (Jest)
  2. Интеграционные тесты: API эндпоинты (Supertest)
  3. E2E тесты: полный поток с фронтендом (Cypress/Playwright)
  4. Нагрузочное тестирование: сравнение производительности с 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 хешей

Организационные риски

  1. Время миграции: оценка 2-4 недели для одного разработчика
  2. Обучение команды: если команда не знает TypeScript/Node.js
  3. Поддержка двух кодовых баз: в период параллельной работы

Преимущества перехода на Node.js

Технические преимущества

  1. Единый язык: JavaScript/TypeScript на фронтенде и бэкенде
  2. Большая экосистема: npm с огромным количеством пакетов
  3. Высокая производительность: Node.js хорошо справляется с I/O операциями
  4. Меньшее потребление памяти: по сравнению с Python + FastAPI

Бизнес-преимущества

  1. Упрощение найма: больше разработчиков знают JavaScript чем Python
  2. Более быстрое развитие: возможность переиспользования кода между фронтендом и бэкендом
  3. Лучшая масштабируемость: Node.js лучше подходит для микросервисной архитектуры
  4. Снижение затрат на инфраструктуру: более эффективное использование ресурсов

Рекомендации по внедрению

Приоритетность задач

  1. Высокий приоритет:

    • Настройка базовой инфраструктуры (Express, TypeORM, Docker)
    • Реализация аутентификации (JWT, bcrypt)
    • Миграция основных сущности (User, Course)
  2. Средний приоритет:

    • Реализация оставшихся эндпоинтов
    • Настройка Swagger документации
    • Интеграционное тестирование
  3. Низкий приоритет:

    • Оптимизация производительности
    • Расширенные функции (пагинация, сортировка)
    • Мониторинг и логирование

Контрольные точки

  1. Неделя 1: Базовая инфраструктура, подключение к БД, простые CRUD операции
  2. Неделя 2: Аутентификация, защита эндпоинтов, основные маршруты
  3. Неделя 3: Тестирование, интеграция с фронтендом, документация
  4. Неделя 4: Постепенное переключение, мониторинг, оптимизация

Критерии успеха

  1. Все существующие API эндпоинты работают без изменений фронтенда
  2. Производительность не хуже FastAPI версии
  3. 100% покрытие тестами критической функциональности
  4. Полная документация API и архитектуры

Заключение

Миграция бэкенда с FastAPI на Express + TypeORM является технически выполнимой задачей с четкими преимуществами для долгосрочного развития проекта. Рекомендуемый подход - поэтапная миграция с сохранением обратной совместимости API.

Ключевые факторы успеха:

  1. Сохранение API интерфейса для минимизации изменений фронтенда
  2. Тщательное тестирование каждого эндпоинта
  3. Поэтапное развертывание с возможностью отката
  4. Документирование всех изменений и решений

Предложенный стек (Express + TypeORM) обеспечивает хороший баланс между производительностью, поддерживаемостью и скоростью разработки, делая его подходящим выбором для данного проекта.