Версионирование API в приложениях, использующих RTK Query, затрагивает сразу несколько уровней архитектуры: сетевой слой, описание эндпоинтов, стратегию кэширования и управление жизненным циклом данных. При работе с REST или GraphQL сервисами эволюция API почти неизбежна — меняются поля ответов, добавляются новые эндпоинты, устаревают старые маршруты. RTK Query предоставляет гибкие механизмы, позволяющие изолировать изменения и минимизировать влияние на клиентскую часть.
Наиболее прямолинейный способ управления версиями 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-описаний и рост поддержки кода при увеличении числа версий.
Более гибкая модель предполагает параметризацию версии через
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. Недостаток — сложность отладки и скрытое поведение запросов.
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) остается общей, а различия сосредоточены только в эндпоинтах.
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 от остальной части приложения. Компоненты продолжают работать с единым контрактом данных, несмотря на различия серверных версий.
RTK Query использует систему тегов для инвалидации кэша. При наличии нескольких версий API важно избегать конфликтов между одинаковыми тегами.
tagTypes: ['UserV1', 'UserV2']
getUsers: builder.query({
query: () => 'users',
providesTags: ['UserV1'],
});
getUsersV2: builder.query({
query: () => 'users',
providesTags: ['UserV2'],
});
Разделение тегов предотвращает случайную инвалидацию данных другой версии API. Это особенно важно при параллельной работе старого и нового интерфейсов.
Переход между API версиями обычно проходит поэтапно и требует поддержки нескольких моделей данных одновременно.
Распространенная стратегия — параллельное использование обеих версий с постепенным переключением потребителей:
RTK Query позволяет управлять этим через условное переключение baseQuery:
const baseQuery = fetchBaseQuery({
baseUrl: (args, api, extraOptions) => {
const version = api.getState().apiVersion.current;
return `/api/${version}`;
},
});
Кэш RTK Query привязан к аргументам запроса и reducerPath. При изменении версии API важно учитывать влияние на кэшированные данные.
Если версии несовместимы, требуется принудительное разделение кэша:
reducerPathserializeQueryArgsПример добавления версии в ключ кэша:
getOrders: builder.query({
query: ({ version }) => `orders?v=${version}`,
});
Такой подход гарантирует независимость данных между версиями.
В сложных системах версии API могут сосуществовать внутри одного endpoint-сета с динамическим выбором логики.
getProduct: builder.query({
query: ({ id, version }) => ({
url: `product/${id}`,
params: { version },
}),
});
Этот подход полезен при серверной поддержке нескольких версий через query parameters, но увеличивает сложность клиентского кода.
Версионирование 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.
В случае недоступности новой версии 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;
};
Подобная логика используется при поэтапных релизах и миграциях крупных систем.