Интеграция фронтенд и бэкенд

Архитектура приложения

Наше приложение разделено на две основные части:

  • Фронтенд - Next.js приложение (директория frontend/), отвечающее за пользовательский интерфейс
  • Бэкенд - FastAPI приложение (директория backend/), обеспечивающее API и бизнес-логику

Такое разделение предоставляет несколько преимуществ:

  • Независимая разработка фронтенда и бэкенда
  • Масштабируемость каждого компонента в зависимости от нагрузки
  • Возможность использования API бэкенда для различных клиентов (веб, мобильные приложения, боты)
  • Полный контроль над системой аутентификации без зависимости от внешних провайдеров
  • Простота поддержки и тестирования

Коммуникация между фронтендом и бэкендом

1. API Прокси

Next.js настроен для проксирования запросов к API через встроенную систему переписывания URL. Это позволяет избежать проблем с CORS и упрощает вызовы API из фронтенда.

// frontend/next.config.ts
const nextConfig = {
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api/v1'}/:path*`,
      },
    ];
  },
};

Благодаря этой конфигурации, запросы из браузера к /api/* автоматически перенаправляются к бэкенду, что упрощает написание кода и устраняет проблемы с CORS.

2. Route Handlers в Next.js

Для дополнительной обработки запросов и промежуточной логики, в приложении используются Route Handlers в Next.js (в директории frontend/app/api/).

// frontend/app/api/auth/me/route.ts
export async function GET(request: NextRequest) {
  try {
    // Получаем токен доступа из куки
    const accessToken = request.cookies.get("access_token")?.value;

    if (!accessToken) {
      return NextResponse.json(
        { message: "Not authenticated" },
        { status: 401 }
      );
    }

    // Обращаемся к API бэкенда
    const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/me`, {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    });

    if (!response.ok) {
      return NextResponse.json(
        { message: "Failed to get user" },
        { status: response.status }
      );
    }

    const userData = await response.json();
    return NextResponse.json(userData);
  } catch (error) {
    console.error("Error fetching user:", error);
    return NextResponse.json(
      { message: "Internal server error" },
      { status: 500 }
    );
  }
}

Route Handlers позволяют:

  • Добавлять логику аутентификации на стороне фронтенда
  • Управлять куками и заголовками
  • Форматировать данные перед отправкой на клиентскую сторону
  • Реализовывать кэширование и другие оптимизации

Аутентификация и авторизация

Одно из ключевых преимуществ нашей архитектуры - полный контроль над системой аутентификации. Вместо зависимости от внешних сервисов, мы реализуем собственную систему на основе JWT токенов.

1. Контекст аутентификации

Для управления состоянием аутентификации используется React контекст:

// frontend/contexts/AuthContext.tsx
export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  const refreshUser = async () => {
    try {
      const response = await fetch("/api/auth/me");
      if (response.ok) {
        const userData = await response.json();
        setUser(userData);
      } else {
        setUser(null);
      }
    } catch (error) {
      console.error("Error fetching user:", error);
      setUser(null);
    } finally {
      setIsLoading(false);
    }
  };

  // Логин, регистрация и выход
  const login = async (email: string, password: string) => {
    // ...код логина...
  };

  const register = async (/* параметры */) => {
    // ...код регистрации...
  };

  const logout = async () => {
    // ...код выхода...
  };

  return (
    <AuthContext.Provider
      value={{
        user,
        isLoading,
        isAuthenticated: !!user,
        login,
        register,
        logout,
        refreshUser,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
}

2. Процесс аутентификации

Процесс аутентификации включает следующие шаги:

  1. Пользователь вводит учетные данные на фронтенде
  2. Фронтенд отправляет данные на API endpoint (/api/auth/login)
  3. Route Handler перенаправляет запрос на бэкенд
  4. Бэкенд проверяет учетные данные, генерирует и возвращает JWT токены
  5. Фронтенд сохраняет токены в куках и обновляет состояние пользователя

Этот подход обеспечивает:

  • Безопасную передачу токенов через HTTP-only cookies
  • Автоматическое обновление токенов через механизм refresh токенов
  • Возможность внедрения дополнительных методов аутентификации

Преимущество для мультиплатформенной разработки

Такая архитектура аутентификации позволяет легко интегрировать различных клиентов помимо веб-приложения. Поскольку бэкенд предоставляет стандартизированное API, вы можете разрабатывать:

  • Мобильные приложения (iOS, Android)
  • Боты для мессенджеров (Telegram, Discord)
  • Настольные приложения
  • Интеграции с другими сервисами

Все эти клиенты могут использовать единую систему аутентификации и авторизации.

Взаимодействие с данными

1. Серверные компоненты

В Next.js 13+ мы используем серверные компоненты для загрузки данных непосредственно с бэкенда:

// Серверный компонент в Next.js
export default async function DashboardPage() {
  // Данные загружаются на сервере
  const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/me`, {
    headers: {
      // Получение cookie в серверном компоненте
      Cookie: cookies().toString(),
    },
    cache: "no-store",
  });

  const userData = await response.json();

  return (
    <div>
      <h1>Панель управления</h1>
      <UserProfile user={userData} />
    </div>
  );
}

2. Клиентские компоненты

Для интерактивных элементов мы используем клиентские компоненты с асинхронными запросами:

"use client";

// Клиентский компонент в Next.js
export default function ProfileEditor() {
  const [formData, setFormData] = useState({
    firstName: "",
    lastName: "",
    email: "",
  });

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const response = await fetch("/api/user/profile", {
        method: "PUT",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(formData),
      });

      if (!response.ok) {
        throw new Error("Failed to update profile");
      }

      // Обработка успешного ответа
    } catch (error) {
      // Обработка ошибки
    }
  };

  // JSX компонента
}

3. Типизация данных

Для обеспечения согласованности между фронтендом и бэкендом, мы используем типизацию данных:

// frontend/types/api.ts
export interface User {
  id: number;
  email: string;
  first_name?: string;
  last_name?: string;
  is_verified: boolean;
  profile_image_url?: string;
}

export interface LoginResponse {
  access_token: string;
  token_type: string;
  user: User;
}

// Использование типов при запросах
async function fetchUser(): Promise<User> {
  const response = await fetch("/api/auth/me");
  return response.json();
}

Такой подход гарантирует, что структура данных, передаваемая между фронтендом и бэкендом, остается согласованной и предсказуемой.

Обработка ошибок и статусов

Система обработки ошибок обеспечивает согласованное взаимодействие между фронтендом и бэкендом:

1. Коды состояния HTTP

Бэкенд использует стандартные коды состояния HTTP для указания результата операции:

  • 200 OK - Успешный ответ
  • 201 Created - Ресурс успешно создан
  • 400 Bad Request - Ошибка в запросе
  • 401 Unauthorized - Отсутствие аутентификации
  • 403 Forbidden - Недостаточно прав доступа
  • 404 Not Found - Ресурс не найден
  • 500 Internal Server Error - Внутренняя ошибка сервера

2. Структура ответов с ошибками

Для унификации обработки ошибок, все ответы с ошибками имеют согласованную структуру:

// Пример ответа с ошибкой от бэкенда
{
  "detail": "Неверный email или пароль",
  "status_code": 401,
  "error_code": "authentication_failed"
}

// Обработка ошибок на фронтенде
try {
  const response = await fetch("/api/auth/login", {
    // ...
  });

  if (!response.ok) {
    const errorData = await response.json();
    throw new Error(errorData.detail || "Ошибка аутентификации");
  }

  // Обработка успешного ответа
} catch (error) {
  // Отображение ошибки пользователю
}

Интеграция с внешними API

Для взаимодействия с внешними API и сервисами (например, GitHub, аналитика) мы используем промежуточные API эндпоинты:

// frontend/app/api/github/contributions/route.ts
export async function GET() {
  try {
    // Получение токена доступа из куки
    const accessToken = cookies().get("access_token")?.value;

    if (!accessToken) {
      return NextResponse.json(
        { message: "Not authenticated" },
        { status: 401 }
      );
    }

    // Запрос к бэкенд API
    const apiUrl = `${process.env.NEXT_PUBLIC_API_URL}/analytics/github/contributions`;
    const response = await fetch(apiUrl, {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
      cache: "no-store",
    });

    if (!response.ok) {
      throw new Error(`API error: ${response.status}`);
    }

    const data = await response.json();
    return NextResponse.json(data);
  } catch (error) {
    return NextResponse.json(
      { message: error.message || "Internal server error" },
      { status: 500 }
    );
  }
}

Такой подход позволяет:

  • Скрыть сложность взаимодействия с внешними API от фронтенд-кода
  • Централизованно управлять кэшированием и оптимизацией запросов
  • Добавлять промежуточную логику обработки данных
  • Обеспечить безопасность, не раскрывая ключи API на клиентской стороне

Развёртывание и конфигурация

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

1. Переменные окружения для фронтенда

# frontend/.env.local

# URL бэкенд API
NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1

# Другие переменные окружения для фронтенда
NEXT_PUBLIC_APP_NAME=Boilerplate

2. Переменные окружения для бэкенда

# backend/.env

# Настройки базы данных
POSTGRES_SERVER=localhost
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=boilerplate

# JWT Secret
SECRET_KEY=your-secret-key
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_DAYS=7

# CORS - разрешенные домены фронтенда
CORS_ORIGINS=http://localhost:3000

3. Docker Compose (опционально)

Для локальной разработки может использоваться Docker Compose, объединяющий фронтенд, бэкенд и базу данных:

# docker-compose.yml
version: '3'

services:
  frontend:
    build:
      context: ./frontend
    ports:
      - "3000:3000"
    environment:
      - NEXT_PUBLIC_API_URL=http://backend:8000/api/v1
    depends_on:
      - backend

  backend:
    build:
      context: ./backend
    ports:
      - "8000:8000"
    environment:
      - POSTGRES_SERVER=db
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=boilerplate
      - CORS_ORIGINS=http://localhost:3000
    depends_on:
      - db

  db:
    image: postgres:14
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=boilerplate
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Заключение

Архитектура с разделением на фронтенд (Next.js) и бэкенд (FastAPI) обеспечивает гибкость, масштабируемость и контроль над всеми аспектами приложения. Это позволяет:

  • Независимо развивать и масштабировать фронтенд и бэкенд
  • Полностью контролировать систему аутентификации без зависимости от внешних провайдеров
  • Легко разрабатывать дополнительные клиенты (мобильные приложения, боты) с использованием того же API
  • Оптимизировать производительность с помощью кэширования и стратегий загрузки данных
  • Поддерживать высокие стандарты типобезопасности и согласованности интерфейсов

Такой подход хоть и требует больше начальных усилий при настройке, но значительно упрощает поддержку и расширение приложения в долгосрочной перспективе.