Документирование API в проектах с использованием RTK Query выполняет сразу несколько задач:
В RTK Query документация особенно важна, поскольку большая часть
сетевой логики концентрируется внутри createApi, а значит
становится единым источником данных о взаимодействии приложения с
сервером.
Типичная структура API-слайса:
import { createApi, fetchBaseQuery } fr om '@reduxjs/toolkit/query/react';
export const postsApi = createApi({
reducerPath: 'postsApi',
baseQuery: fetchBaseQuery({
baseUrl: '/api'
}),
tagTypes: ['Post'],
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts'
}),
getPost: builder.query({
query: (id) => `/posts/${id}`
}),
createPost: builder.mutation({
query: (body) => ({
url: '/posts',
method: 'POST',
body
})
})
})
});
Без документации такой API быстро превращается в набор трудно читаемых endpoints. Особенно это заметно в крупных проектах с десятками сущностей и сложной системой тегов.
Наиболее распространённый подход — использование JSDoc-комментариев.
Пример:
/**
* API для работы с публикациями.
*
* Содержит:
* - получение списка публикаций;
* - получение отдельной публикации;
* - создание публикаций.
*/
export const postsApi = createApi({
Каждый endpoint желательно описывать отдельно.
endpoints: (builder) => ({
/**
* Получение списка публикаций.
*
* Метод: GET
* URL: /posts
*
* Возвращает:
* - массив публикаций;
* - метаинформацию пагинации.
*/
getPosts: builder.query({
query: () => '/posts'
})
})
/**
* Создание публикации.
*
* Метод: POST
* URL: /posts
*
* body:
* {
* title: string,
* content: string
* }
*/
createPost: builder.mutation({
query: (body) => ({
url: '/posts',
method: 'POST',
body
})
})
Важно фиксировать:
Пример:
/**
* Получение публикации по идентификатору.
*
* @param {number} id Идентификатор публикации.
*/
getPost: builder.query({
query: (id) => `/posts/${id}`
})
Во многих проектах endpoint принимает объект параметров.
Пример:
getPosts: builder.query({
query: ({ page, lim it, sort }) => ({
url: '/posts',
params: {
page,
limit,
sort
}
})
})
Такой код без документации быстро становится неочевидным.
Подробное описание:
/**
* Получение списка публикаций.
*
* @param {Object} params
* @param {number} params.page Номер страницы.
* @param {number} params.limit Количество элементов.
* @param {'date' | 'rating'} params.sort Тип сортировки.
*/
RTK Query активно использует tags для инвалидации кэша.
Без документации понять логику тегов крайне сложно.
Пример:
tagTypes: ['Post', 'User', 'Comment']
Рекомендуется документировать назначение каждого тега:
/**
* Post:
* - список публикаций;
* - отдельные публикации.
*
* User:
* - профиль пользователя;
* - настройки пользователя.
*
* Comment:
* - комментарии публикаций.
*/
tagTypes: ['Post', 'User', 'Comment']
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Post']
})
Подробное описание:
/**
* Кэширует:
* - список публикаций;
* - отдельные элементы списка.
*
* Инвалидируется:
* - после createPost;
* - после updatePost;
* - после deletePost.
*/
createPost: builder.mutation({
query: (body) => ({
url: '/posts',
method: 'POST',
body
}),
invalidatesTags: ['Post']
})
Описание:
/**
* После успешного создания публикации
* выполняется инвалидирование:
*
* - списка публикаций;
* - связанных query с тегом Post.
*/
Многие API трансформируют ответ сервера.
Пример:
getPosts: builder.query({
query: () => '/posts',
transformResponse: (response) => {
return response.data;
}
})
Без описания трудно понять:
Рекомендуется:
/**
* Сервер возвращает:
* {
* data: Post[],
* meta: Object
* }
*
* transformResponse извлекает только массив data.
*/
transformErrorResponse: (response) => {
return response.data.message;
}
Описание:
/**
* Преобразует ошибку сервера
* в строковое сообщение для UI.
*/
baseQuery является фундаментом всего API-слайса.
Пример:
baseQuery: fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token;
if (token) {
headers.set('authorization', `Bearer ${token}`);
}
return headers;
}
})
Документация должна описывать:
/**
* Добавляет JWT-токен
* ко всем запросам API.
*
* Формат:
* Authorization: Bearer <token>
*/
Если используется retry:
import { retry } fr om '@reduxjs/toolkit/query';
const baseQuery = retry(fetchBaseQuery({
baseUrl: '/api'
}), {
maxRetries: 3
});
Важно фиксировать:
Большинство ошибок возникает именно в авторизации.
Следует документировать:
Пример:
/**
* При получении 401:
* - выполняется запрос refreshToken;
* - access token обновляется;
* - исходный запрос повторяется.
*
* Если refresh неуспешен:
* - пользователь разлогинивается.
*/
Очень важно фиксировать реальные контракты API.
Пример плохой практики:
getUser: builder.query({
query: (id) => `/users/${id}`
})
Из кода непонятно:
Лучше:
/**
* Response:
* {
* id: number,
* name: string,
* email: string,
* avatar: string | null,
* role: 'admin' | 'user'
* }
*/
Следует описывать:
Пример:
/**
* Возможные ошибки:
*
* 400:
* - неверные данные формы.
*
* 401:
* - пользователь не авторизован.
*
* 404:
* - публикация не найдена.
*
* 500:
* - внутренняя ошибка сервера.
*/
Пагинация — один из самых сложных участков API.
Пример:
getPosts: builder.query({
query: ({ page, lim it }) => ({
url: '/posts',
params: {
page,
limit
}
})
})
Документация должна фиксировать:
Пример описания:
/**
* Используется cursor pagination.
*
* Query params:
* - cursor
* - limit
*
* Response:
* {
* items: [],
* nextCursor: string | null
* }
*/
RTK Query содержит сложную систему кэширования.
Без описания поведение API может быть непредсказуемым.
Следует документировать:
Пример:
/**
* Кэш хранится 300 секунд
* после удаления последнего подписчика.
*/
keepUnusedDataFor: 300
useGetNotificationsQuery(undefined, {
pollingInterval: 5000
});
Описание:
/**
* Обновление уведомлений
* выполняется каждые 5 секунд.
*/
Optimistic updates требуют максимально подробной документации.
Пример:
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
postsApi.util.updateQueryData(
'getPosts',
undefined,
(draft) => {
draft.push(arg);
}
)
);
try {
await queryFulfilled;
} catch {
patchResult.undo();
}
}
Необходимо описывать:
/**
* Оптимистически добавляет публикацию
* в локальный кэш списка.
*
* При ошибке:
* - изменения откатываются.
*/
Если используется WebSocket или SSE:
onCacheEntryAdded: async (
arg,
{
updateCachedData,
cacheDataLoaded,
cacheEntryRemoved
}
) => {
Следует документировать:
RTK Query содержит несколько lifecycle hooks:
Каждый hook должен быть подробно описан.
Пример:
/**
* Lifecycle:
*
* 1. Выполняется optimistic update.
* 2. Отправляется запрос.
* 3. При успехе данные сохраняются.
* 4. При ошибке выполняется rollback.
*/
При использовании OpenAPI codegen важно фиксировать:
Пример:
/**
* Файл сгенерирован автоматически.
*
* Источник:
* /openapi/schema.json
*
* Ручное редактирование запрещено.
*/
RTK Query поддерживает injectEndpoints.
Пример:
const extendedApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
getProfile: builder.query({
query: () => '/profile'
})
})
});
Важно фиксировать:
injectEndpoints({
overrideExisting: true
})
Описание:
/**
* Разрешает переопределение
* уже существующих endpoints.
*
* Используется:
* - в development;
* - при hot reload;
* - в тестовой среде.
*/
В крупных проектах необходимо описывать:
src/
├── services/
│ ├── api/
│ │ ├── baseApi.js
│ │ ├── authApi.js
│ │ ├── postsApi.js
│ │ └── usersApi.js
Описание:
baseApi содержит общий baseQuery;Следует фиксировать правила именования:
getPosts
getPost
getUser
createPost
updatePost
deletePost
useLazyGetPostsQuery
useCreatePostMutation
Крупные команды обычно фиксируют:
Любое нестандартное поведение должно сопровождаться пояснением.
Плохой пример:
queryFn: async () => {
Без комментариев невозможно понять причину использования
queryFn.
Хороший пример:
/**
* queryFn используется вместо query,
* поскольку endpoint объединяет
* несколько последовательных запросов.
*/
queryFn: async (arg, api, extraOptions, baseQuery) => {
const userResult = await baseQuery('/user');
if (userResult.error) {
return userResult;
}
const postsResult = await baseQuery('/posts');
return {
data: {
user: userResult.data,
posts: postsResult.data
}
};
}
Описание:
/**
* Endpoint агрегирует:
* - профиль пользователя;
* - список публикаций.
*
* Выполняет два последовательных запроса.
*/
RTK Query часто используется вместе с:
В таких проектах документация делится на два уровня:
Пример:
/**
* Endpoints сгенерированы
* из OpenAPI schema v2.4.
*
* Генерация:
* npm run generate-api
*/
Даже в JavaScript желательно документировать формы объектов.
Пример:
/**
* @typedef {Object} Post
*
* @property {number} id
* @property {string} title
* @property {string} content
* @property {string} createdAt
*/
Использование:
/**
* @returns {Post[]}
*/
Mutation endpoints часто содержат side effects:
Подобное поведение должно быть явно описано.
Пример:
/**
* После успешного логина:
* - access token сохраняется в store;
* - refresh token сохраняется в cookie;
* - пользователь перенаправляется в dashboard.
*/
Важно фиксировать ограничения:
uploadFile: builder.mutation({
query: (file) => {
const formData = new FormData();
formData.append('file', file);
return {
url: '/upload',
method: 'POST',
body: formData
};
}
})
Описание:
/**
* Максимальный размер файла: 10MB.
*
* Поддерживаемые форматы:
* - jpg
* - png
* - pdf
*/