Интеграция с внешними сервисами

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

Базовый вариант опирается на fetchBaseQuery, представляющий собой тонкую обёртку над fetch с добавленными возможностями сериализации, обработки заголовков и ошибок.

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

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

Внешний сервис в этой модели рассматривается как источник REST-ресурсов, а каждый endpoint описывает конкретный контракт взаимодействия.


Конфигурация заголовков для внешних сервисов

Практически любой внешний API требует авторизации: API key, JWT или OAuth токены. В RTK Query это решается через prepareHeaders, который позволяет динамически формировать заголовки перед каждым запросом.

baseQuery: fetchBaseQuery({
  baseUrl: 'https://api.example.com/v1',
  prepareHeaders: (headers, { getState }) => {
    const token = getState().auth.token;

    if (token) {
      headers.set('authorization', `Bearer ${token}`);
    }

    headers.set('x-api-key', process.env.API_KEY);

    return headers;
  },
});

Такой подход делает интеграцию с внешними сервисами централизованной: логика авторизации не размазывается по компонентам или thunk-слоям.


Работа с несколькими внешними сервисами

Сложные приложения часто взаимодействуют сразу с несколькими API: например, основной backend, сервис аналитики и сторонний геокодер. RTK Query позволяет реализовать это через несколько API-слайсов.

const mainApi = createApi({
  reducerPath: 'mainApi',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: (builder) => ({
    getPosts: builder.query({
      query: () => '/posts',
    }),
  }),
});

const geoApi = createApi({
  reducerPath: 'geoApi',
  baseQuery: fetchBaseQuery({
    baseUrl: 'https://geo.external-service.com',
  }),
  endpoints: (builder) => ({
    getCoordinates: builder.query({
      query: (city) => `/coords?city=${city}`,
    }),
  }),
});

Такое разделение упрощает масштабирование и позволяет изолировать правила кэширования и инвалидации для каждого внешнего источника.


Инкапсуляция логики API через custom baseQuery

При интеграции с внешними сервисами часто возникает необходимость унифицировать ошибки, логировать запросы или автоматически обновлять токены. Для этого создаётся обёртка над базовым fetchBaseQuery.

const rawBaseQuery = fetchBaseQuery({
  baseUrl: 'https://api.example.com',
});

const baseQueryWithReauth = async (args, api, extraOptions) => {
  let result = await rawBaseQuery(args, api, extraOptions);

  if (result.error && result.error.status === 401) {
    const refreshResult = await rawBaseQuery(
      '/auth/refresh',
      api,
      extraOptions
    );

    if (refreshResult.data) {
      api.dispatch({ type: 'auth/setCredentials', payload: refreshResult.data });

      result = await rawBaseQuery(args, api, extraOptions);
    }
  }

  return result;
};

Такая прослойка превращает RTK Query в адаптер для нестабильных внешних API, где требуется автоматическое восстановление сессии.


Адаптация нестандартных форматов ответов

Внешние сервисы редко придерживаются единого стандарта. Часто требуется трансформация ответа до попадания в кэш RTK Query. Для этого используется transformResponse.

getProducts: builder.query({
  query: () => '/products',
  transformResponse: (response) => {
    return response.items.map((item) => ({
      id: item.product_id,
      title: item.name,
      price: item.cost,
    }));
  },
});

Это позволяет отделить доменную модель приложения от формата внешнего API.


Интеграция с сервисами, требующими query-параметры

Многие внешние API используют query string вместо REST-структуры. RTK Query позволяет формировать параметры декларативно.

getSearchResults: builder.query({
  query: ({ query, page }) => ({
    url: '/search',
    params: {
      q: query,
      page,
    },
  }),
});

Такой подход снижает риск ошибок при ручной конкатенации URL и упрощает поддержку сложных фильтров.


Работа с GraphQL через RTK Query

Хотя RTK Query ориентирован на REST, он легко адаптируется под GraphQL через кастомный baseQuery.

const graphqlBaseQuery =
  ({ baseUrl }) =>
  async ({ body }) => {
    const result = await fetch(baseUrl, {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
      },
      body: JSON.stringify(body),
    });

    const data = await result.json();

    if (data.errors) {
      return { error: data.errors };
    }

    return { data: data.data };
  };

Использование:

const api = createApi({
  reducerPath: 'graphqlApi',
  baseQuery: graphqlBaseQuery({
    baseUrl: 'https://graphql.example.com',
  }),
  endpoints: (builder) => ({
    getUser: builder.query({
      query: (id) => ({
        body: {
          query: `
            query ($id: ID!) {
              user(id: $id) {
                id
                name
              }
            }
          `,
          variables: { id },
        },
      }),
    }),
  }),
});

Управление кэшированием при работе с внешними данными

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

const api = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Users', 'Posts'],
  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => '/users',
      providesTags: ['Users'],
    }),
    addUser: builder.mutation({
      query: (user) => ({
        url: '/users',
        method: 'POST',
        body: user,
      }),
      invalidatesTags: ['Users'],
    }),
  }),
});

При работе с внешними сервисами, где данные могут изменяться вне контроля приложения, часто требуется агрессивная инвалидизация или отключение кэширования для критичных endpoints.


Динамическое управление baseUrl для разных окружений

Интеграция с внешними сервисами почти всегда требует разделения окружений: development, staging, production.

baseQuery: fetchBaseQuery({
  baseUrl: process.env.NODE_ENV === 'production'
    ? 'https://api.prod.com'
    : 'https://api.dev.com',
});

В более сложных сценариях baseUrl может зависеть от параметров запроса:

const dynamicBaseQuery = async (args, api, extraOptions) => {
  const service = args.service;

  const baseUrlMap = {
    main: 'https://api.main.com',
    analytics: 'https://analytics.external.com',
  };

  const baseQuery = fetchBaseQuery({
    baseUrl: baseUrlMap[service],
  });

  return baseQuery(args, api, extraOptions);
};

Интеграция с внешними сервисами через WebSocket

Некоторые внешние API предоставляют потоковые данные. RTK Query поддерживает такой сценарий через onCacheEntryAdded.

getLiveUpdates: builder.query({
  query: () => '/stream',
  async onCacheEntryAdded(
    arg,
    { updateCachedData, cacheDataLoaded, cacheEntryRemoved }
  ) {
    await cacheDataLoaded;

    const ws = new WebSocket('wss://api.example.com/live');

    ws.onmess age = (event) => {
      const data = JSON.parse(event.data);

      updateCachedData((draft) => {
        draft.push(data);
      });
    };

    await cacheEntryRemoved;
    ws.close();
  },
});

Таким образом RTK Query превращается в мост между внешними потоковыми сервисами и локальным кэшем приложения.


Обработка ошибок внешних API и нормализация

Внешние сервисы часто возвращают разные форматы ошибок. Для унификации используется слой нормализации внутри baseQuery.

const normalizedBaseQuery = async (args, api, extraOptions) => {
  const result = await fetchBaseQuery({
    baseUrl: 'https://api.example.com',
  })(args, api, extraOptions);

  if (result.error) {
    return {
      error: {
        status: result.error.status,
        message: result.error.data?.message || 'Unknown error',
        code: result.error.data?.code,
      },
    };
  }

  return result;
};

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


Интеграция с сервисами загрузки файлов

Внешние API часто требуют multipart-загрузку. RTK Query позволяет реализовать это через FormData.

uploadFile: builder.mutation({
  query: (file) => {
    const formData = new FormData();
    formData.append('file', file);

    return {
      url: '/upload',
      method: 'POST',
      body: formData,
    };
  },
});

Важно учитывать, что в этом случае заголовок Content-Type не задаётся вручную — браузер формирует его автоматически.


Оркестрация нескольких внешних источников данных

В сложных приложениях данные могут агрегироваться из нескольких API одновременно. RTK Query позволяет комбинировать запросы через queryFn.

getDashboardData: builder.query({
  async queryFn(arg, api, extraOptions, baseQuery) {
    const [users, stats] = await Promise.all([
      baseQuery('/users', api, extraOptions),
      baseQuery('/stats', api, extraOptions),
    ]);

    if (users.error || stats.error) {
      return { error: 'Failed to load dashboard' };
    }

    return {
      data: {
        users: users.data,
        stats: stats.data,
      },
    };
  },
});

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


Политики повторных запросов при нестабильных API

Внешние сервисы часто нестабильны. RTK Query позволяет реализовать retry-логику через кастомный baseQuery.

const baseQueryWithRetry = async (args, api, extraOptions) => {
  let result;

  for (let i = 0; i < 3; i++) {
    result = await fetchBaseQuery({
      baseUrl: 'https://api.example.com',
    })(args, api, extraOptions);

    if (!result.error) break;
  }

  return result;
};

Контроль побочных эффектов при синхронизации с внешними системами

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

updateProfile: builder.mutation({
  query: (data) => ({
    url: '/profile',
    method: 'PATCH',
    body: data,
  }),
  invalidatesTags: ['Profile'],
});

Такая модель снижает связанность кода с внешними API и делает интеграцию предсказуемой даже при сложной бизнес-логике.