REST API best practices

REST-архитектура опирается на ресурсы, стандартные HTTP-методы и предсказуемые состояния сервера. TanStack Query выступает клиентским слоем управления серверным состоянием, где ключевыми становятся кэширование, синхронизация, инвалидация и контроль актуальности данных. Корректная интеграция этих двух подходов требует согласованной модели идентификации ресурсов, стратегий обновления и правил работы с запросами.


Ресурсная модель REST и влияние на клиентское состояние

REST строится вокруг ресурсов, доступных по стабильным URL:

  • GET /users
  • GET /users/:id
  • POST /users
  • PATCH /users/:id
  • DELETE /users/:id

В TanStack Query каждый такой ресурс отражается в виде query key, который должен однозначно соответствовать состоянию данных.

Ключевая идея: query key — это отражение REST-ресурса и его параметров

Пример:

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
})
useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
})

Любая вариативность запроса (фильтры, пагинация, сортировка) должна отражаться в ключе:

queryKey: ['users', { page, limit, sort }]

Отсутствие строгой структуры ключей приводит к конфликтам кэша и некорректной переиспользуемости данных.


HTTP-уровень и согласование с клиентским кэшем

REST предполагает использование семантики HTTP:

  • GET — чтение
  • POST — создание
  • PATCH/PUT — изменение
  • DELETE — удаление

TanStack Query разделяет эти операции на:

  • useQuery — чтение
  • useMutation — изменение

Согласованность достигается за счет того, что mutation всегда должна приводить к инвалидации или обновлению query cache.


Кэширование, staleTime и gcTime

TanStack Query вводит разделение между:

  • fresh data — данные считаются актуальными
  • stale data — данные устарели, но могут отображаться
  • garbage collected data — удаленные из памяти

Ключевые параметры:

staleTime: 1000 * 60 * 5
gcTime: 1000 * 60 * 30

REST API не предоставляет встроенного механизма клиентского кэширования, поэтому TanStack Query компенсирует это логикой:

  • короткий staleTime — высокая актуальность (например, цены, статус)
  • длинный staleTime — редкие изменения (например, справочники)

Серверная модель REST дополняется клиентской стратегией TTL, что снижает количество повторных запросов.


Инвалидация данных как механизм согласования состояния

После мутаций REST-ресурсов необходимо синхронизировать кэш:

const queryClient = useQueryClient()

const mutation = useMutation({
  mutationFn: createUser,
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['users'] })
  },
})

Инвалидация является центральным механизмом согласования REST и клиентского состояния.

Существуют стратегии:

  • Полная инвалидация ресурса (['users'])
  • Точечное обновление (['user', id])
  • Обновление кэша вручную (setQueryData)

Пагинация и работа с коллекциями ресурсов

REST-эндпоинты часто возвращают коллекции с пагинацией:

{
  "data": [],
  "page": 1,
  "totalPages": 10
}

TanStack Query требует включения параметров в ключ:

queryKey: ['users', page]

Для cursor-based pagination используется useInfiniteQuery:

useInfiniteQuery({
  queryKey: ['users'],
  queryFn: ({ pageParam }) => fetchUsers(pageParam),
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

REST с cursor-подходом снижает проблемы с консистентностью данных при изменении коллекции.


Обработка ошибок и политика повторов

REST API может возвращать:

  • 400 — ошибка запроса
  • 401 — отсутствие авторизации
  • 404 — ресурс не найден
  • 500 — серверная ошибка

TanStack Query позволяет задавать стратегию retry:

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  retry: (failureCount, error) => {
    if (error.status === 404) return false
    return failureCount < 3
  },
})

Антипаттерн: автоматический retry для всех ошибок без фильтрации HTTP-статусов.


Оптимистические обновления и согласование с REST

REST предполагает факт завершенности операции только после ответа сервера, однако UX часто требует мгновенного обновления интерфейса.

TanStack Query поддерживает optimistic updates:

useMutation({
  mutationFn: updateUser,
  onMutate: async (newUser) => {
    await queryClient.cancelQueries(['user', newUser.id])

    const previous = queryClient.getQueryData(['user', newUser.id])

    queryClient.setQueryData(['user', newUser.id], newUser)

    return { previous }
  },
  onError: (err, newUser, context) => {
    queryClient.setQueryData(
      ['user', newUser.id],
      context.previous
    )
  },
  onSettled: (data, error, newUser) => {
    queryClient.invalidateQueries(['user', newUser.id])
  },
})

Ключевой принцип: REST остается источником истины, клиент — временная проекция состояния.


Отмена запросов и управление конкурентностью

REST-запросы могут устаревать до завершения выполнения. TanStack Query использует AbortController:

queryFn: async ({ signal }) => {
  const res = await fetch('/api/users', { signal })
  return res.json()
}

Это особенно важно при:

  • быстрых фильтрах
  • поиске
  • переключении страниц

Отмена предотвращает race conditions и лишнюю нагрузку на API.


Слой API и изоляция REST-логики

Корректная архитектура отделяет HTTP-логику от TanStack Query:

// api/users.js
export const fetchUsers = async () => {
  const res = await fetch('/api/users')
  if (!res.ok) throw new Error('Error')
  return res.json()
}
// hooks/useUsers.js
export const useUsers = () =>
  useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  })

Такое разделение обеспечивает:

  • переиспользуемость API-слоя
  • тестируемость
  • независимость от UI

Версионирование REST и влияние на кэш

REST API часто эволюционирует:

  • /api/v1/users
  • /api/v2/users

TanStack Query требует учитывать версию в ключе:

queryKey: ['v2', 'users']

Иначе возможна коллизия данных между версиями API.


Заголовки HTTP и условные запросы

REST поддерживает механизмы оптимизации:

  • ETag
  • If-None-Match
  • Cache-Control

При интеграции с TanStack Query возможно снижение нагрузки:

const res = await fetch('/api/users', {
  headers: {
    'If-None-Match': etag,
  },
})

Сервер может вернуть 304 Not Modified, позволяя сохранить кэш без изменений.


Организация query и mutation слоя

Структура hooks обычно строится по ресурсам:

  • useUsers
  • useUser
  • useCreateUser
  • useUpdateUser
  • useDeleteUser

Каждая мутация должна явно описывать:

  • какие query invalidation выполняются
  • какие optimistic updates применяются
  • какие ключи затрагиваются

Неполная инвалидация приводит к десинхронизации состояния между REST и клиентом.


Антипаттерны интеграции REST и TanStack Query

  • использование случайных query key без структуры
  • отсутствие инвалидации после mutation
  • смешивание API-логики и UI-логики
  • кэширование без учета параметров запроса
  • игнорирование abort signal при поиске и фильтрации
  • чрезмерный retry для всех HTTP ошибок
  • отсутствие разделения между списками и деталями ресурса

Такие подходы приводят к неконсистентному кэшу и дублированию запросов, что противоречит как REST-архитектуре, так и модели TanStack Query.