API запросы

В этом разделе описывается, как выполнять API запросы в Boilerplate приложении. Вы узнаете о различных способах взаимодействия с бэкендом и обработке данных.

Обзор API клиента

В Boilerplate реализован удобный API клиент, который обеспечивает:

  • Типизированные запросы с использованием TypeScript
  • Обработку ошибок и переповторы запросов
  • Автоматическое добавление заголовков аутентификации
  • Кэширование с использованием React Query
  • Обработку состояний загрузки и ошибок

Базовый API клиент

Основной API клиент определен в файле lib/api.ts:

typescript
// 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), которая предоставляет кэширование, автоматические повторы запросов и управление состоянием:

typescript
// 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 в компоненте:

tsx
// 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-маршруты:

tsx
// 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:

typescript
// 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:

typescript
// 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 бэкенда можно использовать автоматическую генерацию типов:

typescript
// 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

Ресурсы