API запросы
В этом разделе описывается, как выполнять API запросы в Boilerplate приложении. Вы узнаете о различных способах взаимодействия с бэкендом и обработке данных.
Обзор API клиента
В Boilerplate реализован удобный API клиент, который обеспечивает:
- Типизированные запросы с использованием TypeScript
- Обработку ошибок и переповторы запросов
- Автоматическое добавление заголовков аутентификации
- Кэширование с использованием React Query
- Обработку состояний загрузки и ошибок
Базовый API клиент
Основной API клиент определен в файле lib/api.ts
:
// lib/api.ts
import { getSession } from "next-auth/react";
interface FetchOptions extends RequestInit {
params?: Record<string, string>;
}
interface ApiError {
message: string;
status: number;
}
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async fetch<T>(endpoint: string, options: FetchOptions = {}): Promise<T> {
const { params, ...fetchOptions } = options;
// Добавляем query параметры, если они есть
const url = new URL(endpoint, this.baseUrl);
if (params) {
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
}
// Получаем сессию для аутентификации
const session = await getSession();
// Формируем заголовки
const headers = new Headers(options.headers);
if (!headers.has("Content-Type")) {
headers.append("Content-Type", "application/json");
}
// Добавляем токен аутентификации, если есть сессия
if (session?.accessToken) {
headers.set("Authorization", `Bearer ${session.accessToken}`);
}
try {
const response = await fetch(url.toString(), {
...fetchOptions,
headers,
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw {
message: errorData.detail || "Произошла ошибка при запросе",
status: response.status,
};
}
// Пустой ответ (например, для DELETE запросов)
if (response.status === 204) {
return {} as T;
}
return await response.json();
} catch (error) {
if ((error as ApiError).status) {
throw error;
}
throw {
message: "Сетевая ошибка",
status: 0,
};
}
}
// Вспомогательные методы для разных типов запросов
get<T>(endpoint: string, options: FetchOptions = {}): Promise<T> {
return this.fetch<T>(endpoint, { ...options, method: "GET" });
}
post<T>(endpoint: string, data: any, options: FetchOptions = {}): Promise<T> {
return this.fetch<T>(endpoint, {
...options,
method: "POST",
body: JSON.stringify(data),
});
}
put<T>(endpoint: string, data: any, options: FetchOptions = {}): Promise<T> {
return this.fetch<T>(endpoint, {
...options,
method: "PUT",
body: JSON.stringify(data),
});
}
patch<T>(endpoint: string, data: any, options: FetchOptions = {}): Promise<T> {
return this.fetch<T>(endpoint, {
...options,
method: "PATCH",
body: JSON.stringify(data),
});
}
delete<T>(endpoint: string, options: FetchOptions = {}): Promise<T> {
return this.fetch<T>(endpoint, { ...options, method: "DELETE" });
}
}
// Создаем экземпляр API клиента
const api = new ApiClient(process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000");
export default api;
Использование с React Query
Для более эффективной работы с API используется библиотека React Query (TanStack Query), которая предоставляет кэширование, автоматические повторы запросов и управление состоянием:
// lib/queries/users.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import api from "@/lib/api";
import { User, CreateUserInput, UpdateUserInput } from "@/types";
// Ключи запросов для кэширования
export const userKeys = {
all: ["users"] as const,
lists: () => [...userKeys.all, "list"] as const,
list: (filters: Record<string, string>) => [...userKeys.lists(), filters] as const,
details: () => [...userKeys.all, "detail"] as const,
detail: (id: number) => [...userKeys.details(), id] as const,
};
// Хук для получения списка пользователей
export function useUsers(filters: Record<string, string> = {}) {
return useQuery({
queryKey: userKeys.list(filters),
queryFn: () => api.get<User[]>("/users", { params: filters }),
});
}
// Хук для получения одного пользователя
export function useUser(id: number) {
return useQuery({
queryKey: userKeys.detail(id),
queryFn: () => api.get<User>(`/users/${id}`),
enabled: !!id, // Запрос выполняется только если id существует
});
}
// Хук для создания пользователя
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateUserInput) => api.post<User>("/users", data),
onSuccess: () => {
// Инвалидируем кэш списка пользователей после успешного создания
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
},
});
}
// Хук для обновления пользователя
export function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: UpdateUserInput }) =>
api.patch<User>(`/users/${id}`, data),
onSuccess: (data) => {
// Обновляем кэш после успешного обновления
queryClient.invalidateQueries({ queryKey: userKeys.detail(data.id) });
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
},
});
}
// Хук для удаления пользователя
export function useDeleteUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => api.delete<void>(`/users/${id}`),
onSuccess: (_, id) => {
// Инвалидируем кэш после успешного удаления
queryClient.invalidateQueries({ queryKey: userKeys.detail(id) });
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
},
});
}
Использование API в компонентах
Пример использования API в компоненте:
// components/users/UserList.tsx
"use client";
import { useState } from "react";
import { useUsers, useDeleteUser } from "@/lib/queries/users";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
export function UserList() {
const [page, setPage] = useState(1);
const { data: users, isLoading, error } = useUsers({ page: page.toString() });
const deleteUser = useDeleteUser();
const handleDelete = async (id: number) => {
try {
await deleteUser.mutateAsync(id);
toast.success("Пользователь успешно удален");
} catch (error) {
toast.error("Ошибка при удалении пользователя");
}
};
if (isLoading) return <div>Загрузка...</div>;
if (error) return <div>Ошибка: {(error as Error).message}</div>;
return (
<div>
<h2 className="text-xl font-bold mb-4">Список пользователей</h2>
<div className="space-y-4">
{users?.map((user) => (
<div key={user.id} className="p-4 border rounded-lg flex justify-between items-center">
<div>
<p className="font-semibold">{user.name}</p>
<p className="text-sm text-gray-400">{user.email}</p>
</div>
<Button
variant="destructive"
size="sm"
onClick={() => handleDelete(user.id)}
disabled={deleteUser.isPending}
>
Удалить
</Button>
</div>
))}
</div>
<div className="flex gap-2 mt-4">
<Button
variant="outline"
onClick={() => setPage(p => Math.max(p - 1, 1))}
disabled={page === 1}
>
Назад
</Button>
<span className="flex items-center px-2">Страница {page}</span>
<Button
variant="outline"
onClick={() => setPage(p => p + 1)}
disabled={!users || users.length === 0}
>
Вперед
</Button>
</div>
</div>
);
}
Server Actions
В Next.js 15 вы можете использовать Server Actions для выполнения серверных запросов без необходимости создавать API-маршруты:
// app/actions/users.ts
"use server";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { db } from "@/lib/db";
// Схема валидации для создания пользователя
const createUserSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
role: z.enum(["user", "admin"]).default("user"),
});
export async function createUser(formData: FormData) {
try {
// Получаем и валидируем данные из формы
const data = {
name: formData.get("name") as string,
email: formData.get("email") as string,
role: formData.get("role") as string,
};
const validatedData = createUserSchema.parse(data);
// Создаем пользователя в базе данных
await db.user.create({
data: validatedData,
});
// Инвалидируем кэш для обновления UI
revalidatePath("/users");
return { success: true, message: "Пользователь успешно создан" };
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
message: "Ошибка валидации данных",
errors: error.errors
};
}
return {
success: false,
message: "Не удалось создать пользователя"
};
}
}
// Использование в компоненте:
// app/users/create/page.tsx
export default function CreateUserPage() {
return (
<form action={createUser}>
<input name="name" placeholder="Имя" required />
<input name="email" type="email" placeholder="Email" required />
<select name="role">
<option value="user">Пользователь</option>
<option value="admin">Администратор</option>
</select>
<button type="submit">Создать</button>
</form>
);
}
API маршруты (Route Handlers)
Для создания собственных API маршрутов на стороне фронтенда используются Route Handlers в директории app/api:
// app/api/webhook/route.ts
import { NextRequest, NextResponse } from "next/server";
import { headers } from "next/headers";
export async function POST(request: NextRequest) {
const body = await request.json();
const headersList = headers();
// Проверка подписи вебхука (пример)
const signature = headersList.get("x-signature");
if (!signature || !verifySignature(body, signature)) {
return NextResponse.json(
{ error: "Недействительная подпись" },
{ status: 401 }
);
}
try {
// Обработка данных вебхука
await processWebhookEvent(body);
return NextResponse.json(
{ success: true, message: "Вебхук успешно обработан" },
{ status: 200 }
);
} catch (error) {
console.error("Ошибка обработки вебхука:", error);
return NextResponse.json(
{ error: "Внутренняя ошибка сервера" },
{ status: 500 }
);
}
}
function verifySignature(payload: any, signature: string): boolean {
// Логика проверки подписи
return true; // Заглушка
}
async function processWebhookEvent(data: any) {
// Логика обработки вебхука
console.log("Обработка события вебхука:", data.event);
}
// GET запросы также можно обрабатывать:
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get("q");
return NextResponse.json({
message: `Получен параметр: ${query || "нет"}`
});
}
Обработка ошибок
В Boilerplate реализована централизованная обработка ошибок API:
// lib/error-handler.ts
import { toast } from "sonner";
interface ApiError {
message: string;
status: number;
errors?: Record<string, string[]>;
}
export function handleApiError(error: unknown) {
if ((error as ApiError).status) {
const apiError = error as ApiError;
// Обрабатываем различные типы ошибок
switch (apiError.status) {
case 401:
toast.error("Требуется аутентификация");
// Можно добавить перенаправление на страницу входа
break;
case 403:
toast.error("Доступ запрещен");
break;
case 404:
toast.error("Ресурс не найден");
break;
case 422:
// Ошибки валидации
if (apiError.errors) {
Object.values(apiError.errors).forEach(errors => {
errors.forEach(errorMessage => {
toast.error(errorMessage);
});
});
} else {
toast.error(apiError.message || "Ошибка валидации данных");
}
break;
case 429:
toast.error("Слишком много запросов. Пожалуйста, повторите позже");
break;
case 500:
case 502:
case 503:
toast.error("Ошибка сервера. Пожалуйста, повторите позже");
break;
default:
toast.error(apiError.message || "Произошла неизвестная ошибка");
}
} else {
// Общая ошибка (не от API)
toast.error("Не удалось выполнить запрос");
console.error(error);
}
}
// Использование:
// try {
// await api.post("/some-endpoint", data);
// } catch (error) {
// handleApiError(error);
// }
Интеграция с бэкендом
Для обеспечения типовой безопасности при работе с API бэкенда можно использовать автоматическую генерацию типов:
// types/api.ts
// Типы, соответствующие моделям из FastAPI бэкенда
export interface User {
id: number;
email: string;
full_name: string;
is_active: boolean;
is_superuser: boolean;
created_at: string;
}
export interface CreateUserInput {
email: string;
password: string;
full_name?: string;
is_active?: boolean;
is_superuser?: boolean;
}
export interface UpdateUserInput {
email?: string;
password?: string;
full_name?: string;
is_active?: boolean;
is_superuser?: boolean;
}
// ... другие типы моделей
// В продакшн окружении можно использовать автоматическую генерацию типов
// из OpenAPI спецификации бэкенда с помощью инструментов вроде openapi-typescript
Советы и лучшие практики
- Используйте React Query для кэширования и управления состоянием запросов
- Для глобального состояния используйте Context API или Zustand
- Применяйте типы для запросов и ответов API
- Централизуйте обработку ошибок API
- Используйте дебаунс для поисковых запросов
- Реализуйте механизм переповторов для критически важных запросов
- Применяйте оптимистичные обновления UI для улучшения UX