Управление версиями API

Версионирование API в приложениях, использующих RTK Query, затрагивает сразу несколько уровней архитектуры: сетевой слой, описание эндпоинтов, стратегию кэширования и управление жизненным циклом данных. При работе с REST или GraphQL сервисами эволюция API почти неизбежна — меняются поля ответов, добавляются новые эндпоинты, устаревают старые маршруты. RTK Query предоставляет гибкие механизмы, позволяющие изолировать изменения и минимизировать влияние на клиентскую часть.


Базовая модель версионирования на уровне baseQuery

Наиболее прямолинейный способ управления версиями API — разделение базового URL.

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const apiV1 = createApi({
  reducerPath: 'apiV1',
  baseQuery: fetchBaseQuery({
    baseUrl: '/api/v1',
  }),
  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => 'users',
    }),
  }),
});

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

export const apiV2 = createApi({
  reducerPath: 'apiV2',
  baseQuery: fetchBaseQuery({
    baseUrl: '/api/v2',
  }),
  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => 'users',
    }),
  }),
});

Основная проблема данного подхода — повторение одинаковых endpoint-описаний и рост поддержки кода при увеличении числа версий.


Версионирование через динамический baseUrl

Более гибкая модель предполагает параметризацию версии через prepareHeaders или кастомный baseQuery.

const baseQuery = fetchBaseQuery({
  baseUrl: '/api',
  prepareHeaders: (headers, { getState }) => {
    const version = getState().apiVersion.current;
    headers.set('X-API-Version', version);
    return headers;
  },
});

Здесь версия передается через заголовок. Такой подход используется, когда сервер поддерживает версионирование через headers, а не через URL.

Преимущество модели — отсутствие необходимости дублировать API slices. Недостаток — сложность отладки и скрытое поведение запросов.


Унификация API через injectEndpoints

RTK Query поддерживает расширение API через injectEndpoints, что позволяет централизовать базовую конфигурацию и разносить версии по модулям.

export const baseApi = createApi({
  reducerPath: 'baseApi',
  baseQuery: fetchBaseQuery({
    baseUrl: '/api/v1',
  }),
  endpoints: () => ({}),
});

Добавление версии:

export const extendedApiV1 = baseApi.injectEndpoints({
  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => 'users',
    }),
  }),
});

Для новой версии можно создать отдельный слой расширения:

export const extendedApiV2 = baseApi.injectEndpoints({
  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => 'users?expanded=true',
    }),
  }),
});

Ключевая особенность заключается в том, что базовая инфраструктура RTK Query (store, middleware, caching layer) остается общей, а различия сосредоточены только в эндпоинтах.


Разделение версий через reducerPath

RTK Query использует reducerPath как идентификатор кеша. Это позволяет полностью изолировать версии API на уровне Redux store.

export const apiV1 = createApi({
  reducerPath: 'apiV1',
  baseQuery: fetchBaseQuery({ baseUrl: '/api/v1' }),
  endpoints: (builder) => ({
    getProfile: builder.query({
      query: () => 'profile',
    }),
  }),
});
export const apiV2 = createApi({
  reducerPath: 'apiV2',
  baseQuery: fetchBaseQuery({ baseUrl: '/api/v2' }),
  endpoints: (builder) => ({
    getProfile: builder.query({
      query: () => 'profile',
    }),
  }),
});

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


Совместное использование данных между версиями

При частичной совместимости API возникает необходимость переиспользования данных между версиями. RTK Query позволяет контролировать кэширование через serializeQueryArgs и merge.

getUsers: builder.query({
  query: () => 'users',
  serializeQueryArgs: ({ endpointName }) => endpointName,
  merge: (currentCache, newItems) => {
    currentCache.push(...newItems);
  },
});

Для миграционных сценариев данные из v1 могут использоваться как fallback для v2, если структура не изменилась.


Версионирование через трансформацию данных

Одним из устойчивых подходов является нормализация различий версий на уровне transformResponse.

getUser: builder.query({
  query: (id) => `user/${id}`,
  transformResponse: (response, meta, arg) => {
    if (response.version === 2) {
      return {
        id: response.id,
        fullName: response.profile.name,
      };
    }

    return {
      id: response.id,
      fullName: response.name,
    };
  },
});

Этот слой абстракции позволяет скрыть различия API от остальной части приложения. Компоненты продолжают работать с единым контрактом данных, несмотря на различия серверных версий.


Управление тегами между версиями API

RTK Query использует систему тегов для инвалидации кэша. При наличии нескольких версий API важно избегать конфликтов между одинаковыми тегами.

tagTypes: ['UserV1', 'UserV2']
getUsers: builder.query({
  query: () => 'users',
  providesTags: ['UserV1'],
});
getUsersV2: builder.query({
  query: () => 'users',
  providesTags: ['UserV2'],
});

Разделение тегов предотвращает случайную инвалидацию данных другой версии API. Это особенно важно при параллельной работе старого и нового интерфейсов.


Миграционные стратегии между версиями

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

Распространенная стратегия — параллельное использование обеих версий с постепенным переключением потребителей:

  • часть запросов остается на v1
  • новые модули используют v2
  • общие утилиты нормализуют данные

RTK Query позволяет управлять этим через условное переключение baseQuery:

const baseQuery = fetchBaseQuery({
  baseUrl: (args, api, extraOptions) => {
    const version = api.getState().apiVersion.current;
    return `/api/${version}`;
  },
});

Кэширование при смене версий

Кэш RTK Query привязан к аргументам запроса и reducerPath. При изменении версии API важно учитывать влияние на кэшированные данные.

Если версии несовместимы, требуется принудительное разделение кэша:

  • изменение reducerPath
  • добавление версии в query args
  • использование serializeQueryArgs

Пример добавления версии в ключ кэша:

getOrders: builder.query({
  query: ({ version }) => `orders?v=${version}`,
});

Такой подход гарантирует независимость данных между версиями.


Динамическая маршрутизация эндпоинтов

В сложных системах версии API могут сосуществовать внутри одного endpoint-сета с динамическим выбором логики.

getProduct: builder.query({
  query: ({ id, version }) => ({
    url: `product/${id}`,
    params: { version },
  }),
});

Этот подход полезен при серверной поддержке нескольких версий через query parameters, но увеличивает сложность клиентского кода.


Интеграция с feature flags

Версионирование API часто связано с feature flags. RTK Query позволяет интегрировать флаги через baseQuery или middleware.

const baseQuery = fetchBaseQuery({
  baseUrl: '/api',
  prepareHeaders: (headers, { getState }) => {
    const flags = getState().features;
    if (flags.newApiEnabled) {
      headers.set('X-API-Version', '2');
    }
    return headers;
  },
});

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


Ошибки и несовместимость версий

При переходе между версиями API часто возникают ошибки структуры ответа. RTK Query предоставляет доступ к transformErrorResponse, позволяя унифицировать обработку:

getUser: builder.query({
  query: (id) => `user/${id}`,
  transformErrorResponse: (response) => {
    return {
      status: response.status,
      message: response.data?.error || 'Unknown error',
    };
  },
});

Это особенно важно, когда разные версии API возвращают различные форматы ошибок.


Изоляция логики версий через архитектуру модулей

При масштабировании приложения версии API часто выносятся в отдельные модули:

api/
  v1/
    endpoints.js
    types.js
  v2/
    endpoints.js
    types.js

RTK Query хорошо сочетается с такой структурой благодаря возможности независимых createApi или injectEndpoints.


Долгоживущие кеши и стабильность контрактов

Версионирование API тесно связано с жизненным циклом кэша. При длительном сосуществовании нескольких версий необходимо учитывать:

  • время жизни данных (keepUnusedDataFor)
  • совместимость структуры ответа
  • стратегию очистки кэша при переключении версии
createApi({
  keepUnusedDataFor: 60,
});

Согласованность данных между версиями

При параллельной работе версий API критично поддерживать согласованность данных. Часто используется слой адаптеров, преобразующих ответы в единый формат до попадания в cache RTK Query.

const adaptUser = (user, version) => {
  if (version === 'v2') {
    return {
      id: user.id,
      name: user.profile.name,
    };
  }
  return {
    id: user.id,
    name: user.name,
  };
};

Такой слой снижает зависимость приложения от структуры конкретной версии API.


Стратегии деградации и fallback

В случае недоступности новой версии API возможен fallback на старую:

query: async (arg, api, extraOptions, baseQuery) => {
  let result = await baseQuery({ url: '/v2/users' });

  if (result.error) {
    result = await baseQuery({ url: '/v1/users' });
  }

  return result;
};

Подобная логика используется при поэтапных релизах и миграциях крупных систем.