Интеграция фронтенд и бэкенд
Архитектура приложения
Наше приложение разделено на две основные части:
- Фронтенд - 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. Процесс аутентификации
Процесс аутентификации включает следующие шаги:
- Пользователь вводит учетные данные на фронтенде
- Фронтенд отправляет данные на API endpoint (/api/auth/login)
- Route Handler перенаправляет запрос на бэкенд
- Бэкенд проверяет учетные данные, генерирует и возвращает JWT токены
- Фронтенд сохраняет токены в куках и обновляет состояние пользователя
Этот подход обеспечивает:
- Безопасную передачу токенов через 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
- Оптимизировать производительность с помощью кэширования и стратегий загрузки данных
- Поддерживать высокие стандарты типобезопасности и согласованности интерфейсов
Такой подход хоть и требует больше начальных усилий при настройке, но значительно упрощает поддержку и расширение приложения в долгосрочной перспективе.