Документирование API

Документирование API в проектах с использованием RTK Query выполняет сразу несколько задач:

  • фиксирует структуру сетевого слоя;
  • описывает контракты между клиентом и сервером;
  • упрощает поддержку endpoints;
  • помогает синхронизировать frontend и backend;
  • ускоряет onboarding новых разработчиков;
  • снижает вероятность ошибок при расширении API.

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


Документирование API-слайса

Типичная структура 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

Наиболее распространённый подход — использование JSDoc-комментариев.

Пример:

/**
 * API для работы с публикациями.
 *
 * Содержит:
 * - получение списка публикаций;
 * - получение отдельной публикации;
 * - создание публикаций.
 */
export const postsApi = createApi({

Документирование endpoints

Каждый endpoint желательно описывать отдельно.

Пример query endpoint

endpoints: (builder) => ({
    /**
     * Получение списка публикаций.
     *
     * Метод: GET
     * URL: /posts
     *
     * Возвращает:
     * - массив публикаций;
     * - метаинформацию пагинации.
     */
    getPosts: builder.query({
        query: () => '/posts'
    })
})

Пример mutation endpoint

/**
 * Создание публикации.
 *
 * Метод: POST
 * URL: /posts
 *
 * body:
 * {
 *   title: string,
 *   content: string
 * }
 */
createPost: builder.mutation({
    query: (body) => ({
        url: '/posts',
        method: 'POST',
        body
    })
})

Документирование параметров query

Важно фиксировать:

  • обязательные параметры;
  • типы параметров;
  • допустимые значения;
  • поведение при отсутствии параметров.

Пример:

/**
 * Получение публикации по идентификатору.
 *
 * @param {number} id Идентификатор публикации.
 */
getPost: builder.query({
    query: (id) => `/posts/${id}`
})

Документирование сложных query-объектов

Во многих проектах 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']

Документирование providesTags

getPosts: builder.query({
    query: () => '/posts',

    providesTags: ['Post']
})

Подробное описание:

/**
 * Кэширует:
 * - список публикаций;
 * - отдельные элементы списка.
 *
 * Инвалидируется:
 * - после createPost;
 * - после updatePost;
 * - после deletePost.
 */

Документирование invalidatesTags

createPost: builder.mutation({
    query: (body) => ({
        url: '/posts',
        method: 'POST',
        body
    }),

    invalidatesTags: ['Post']
})

Описание:

/**
 * После успешного создания публикации
 * выполняется инвалидирование:
 *
 * - списка публикаций;
 * - связанных query с тегом Post.
 */

Документирование transformResponse

Многие API трансформируют ответ сервера.

Пример:

getPosts: builder.query({
    query: () => '/posts',

    transformResponse: (response) => {
        return response.data;
    }
})

Без описания трудно понять:

  • какой формат приходит с сервера;
  • какой формат получает приложение;
  • зачем выполняется трансформация.

Рекомендуется:

/**
 * Сервер возвращает:
 * {
 *   data: Post[],
 *   meta: Object
 * }
 *
 * transformResponse извлекает только массив data.
 */

Документирование transformErrorResponse

transformErrorResponse: (response) => {
    return response.data.message;
}

Описание:

/**
 * Преобразует ошибку сервера
 * в строковое сообщение для UI.
 */

Документирование baseQuery

baseQuery является фундаментом всего API-слайса.

Пример:

baseQuery: fetchBaseQuery({
    baseUrl: '/api',

    prepareHeaders: (headers, { getState }) => {
        const token = getState().auth.token;

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

        return headers;
    }
})

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

  • базовый URL;
  • формат авторизации;
  • механизм обновления токена;
  • глобальные заголовки;
  • обработку ошибок.

Документирование prepareHeaders

/**
 * Добавляет JWT-токен
 * ко всем запросам API.
 *
 * Формат:
 * Authorization: Bearer <token>
 */

Документирование retry-логики

Если используется retry:

import { retry } fr om '@reduxjs/toolkit/query';

const baseQuery = retry(fetchBaseQuery({
    baseUrl: '/api'
}), {
    maxRetries: 3
});

Важно фиксировать:

  • количество повторных запросов;
  • условия повторов;
  • исключения;
  • поведение при 401 и 403.

Документирование авторизации

Большинство ошибок возникает именно в авторизации.

Следует документировать:

  • источник токена;
  • механизм refresh token;
  • обработку истечения сессии;
  • поведение logout;
  • автоматический re-auth.

Пример:

/**
 * При получении 401:
 * - выполняется запрос refreshToken;
 * - access token обновляется;
 * - исходный запрос повторяется.
 *
 * Если refresh неуспешен:
 * - пользователь разлогинивается.
 */

Документирование структуры ответов

Очень важно фиксировать реальные контракты API.

Пример плохой практики:

getUser: builder.query({
    query: (id) => `/users/${id}`
})

Из кода непонятно:

  • какие поля возвращаются;
  • какие поля nullable;
  • существуют ли вложенные объекты;
  • как выглядят ошибки.

Лучше:

/**
 * Response:
 * {
 *   id: number,
 *   name: string,
 *   email: string,
 *   avatar: string | null,
 *   role: 'admin' | 'user'
 * }
 */

Документирование ошибок

Следует описывать:

  • HTTP-коды;
  • структуру ошибок;
  • бизнес-ошибки;
  • валидационные ошибки;
  • сетевые ошибки.

Пример:

/**
 * Возможные ошибки:
 *
 * 400:
 * - неверные данные формы.
 *
 * 401:
 * - пользователь не авторизован.
 *
 * 404:
 * - публикация не найдена.
 *
 * 500:
 * - внутренняя ошибка сервера.
 */

Документирование пагинации

Пагинация — один из самых сложных участков API.

Пример:

getPosts: builder.query({
    query: ({ page, lim it }) => ({
        url: '/posts',
        params: {
            page,
            limit
        }
    })
})

Документация должна фиксировать:

  • тип пагинации;
  • номера страниц;
  • лимиты;
  • максимальный размер страницы;
  • формат meta;
  • сортировку.

Cursor pagination

Пример описания:

/**
 * Используется cursor pagination.
 *
 * Query params:
 * - cursor
 * - limit
 *
 * Response:
 * {
 *   items: [],
 *   nextCursor: string | null
 * }
 */

Документирование cache behavior

RTK Query содержит сложную систему кэширования.

Без описания поведение API может быть непредсказуемым.

Следует документировать:

  • время жизни кэша;
  • рефетчинг;
  • polling;
  • refetchOnFocus;
  • refetchOnReconnect;
  • keepUnusedDataFor.

Пример:

/**
 * Кэш хранится 300 секунд
 * после удаления последнего подписчика.
 */
keepUnusedDataFor: 300

Документирование polling

useGetNotificationsQuery(undefined, {
    pollingInterval: 5000
});

Описание:

/**
 * Обновление уведомлений
 * выполняется каждые 5 секунд.
 */

Документирование optimistic updates

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();
    }
}

Необходимо описывать:

  • какие query обновляются;
  • как выполняется rollback;
  • какие данные синхронизируются;
  • возможны ли race conditions.

Документирование updateQueryData

/**
 * Оптимистически добавляет публикацию
 * в локальный кэш списка.
 *
 * При ошибке:
 * - изменения откатываются.
 */

Документирование streaming updates

Если используется WebSocket или SSE:

onCacheEntryAdded: async (
    arg,
    {
        updateCachedData,
        cacheDataLoaded,
        cacheEntryRemoved
    }
) => {

Следует документировать:

  • источник обновлений;
  • тип соединения;
  • события;
  • reconnect;
  • cleanup.

Документирование lifecycle hooks

RTK Query содержит несколько lifecycle hooks:

  • onQueryStarted
  • onCacheEntryAdded

Каждый hook должен быть подробно описан.

Пример:

/**
 * Lifecycle:
 *
 * 1. Выполняется optimistic update.
 * 2. Отправляется запрос.
 * 3. При успехе данные сохраняются.
 * 4. При ошибке выполняется rollback.
 */

Документирование кодогенерации

При использовании OpenAPI codegen важно фиксировать:

  • источник схемы;
  • версию схемы;
  • правила генерации;
  • ручные модификации;
  • запрещённые изменения.

Пример:

/**
 * Файл сгенерирован автоматически.
 *
 * Источник:
 * /openapi/schema.json
 *
 * Ручное редактирование запрещено.
 */

Документирование расширения API

RTK Query поддерживает injectEndpoints.

Пример:

const extendedApi = baseApi.injectEndpoints({
    endpoints: (builder) => ({
        getProfile: builder.query({
            query: () => '/profile'
        })
    })
});

Важно фиксировать:

  • какие модули расширяют API;
  • какие endpoints регистрируются динамически;
  • порядок подключения;
  • возможность override.

Документирование overrideExisting

injectEndpoints({
    overrideExisting: true
})

Описание:

/**
 * Разрешает переопределение
 * уже существующих endpoints.
 *
 * Используется:
 * - в development;
 * - при hot reload;
 * - в тестовой среде.
 */

Документирование архитектуры API

В крупных проектах необходимо описывать:

  • разделение API по доменам;
  • naming conventions;
  • стратегию тегов;
  • структуру файлов;
  • правила расширения;
  • подход к кешированию.

Пример архитектурной документации

src/
├── services/
│   ├── api/
│   │   ├── baseApi.js
│   │   ├── authApi.js
│   │   ├── postsApi.js
│   │   └── usersApi.js

Описание:

  • baseApi содержит общий baseQuery;
  • каждый домен имеет собственный API-модуль;
  • endpoints группируются по бизнес-сущностям;
  • shared logic выносится в baseApi.

Документирование naming conventions

Следует фиксировать правила именования:

Query endpoints

getPosts
getPost
getUser

Mutation endpoints

createPost
updatePost
deletePost

Lazy hooks

useLazyGetPostsQuery

Mutation hooks

useCreatePostMutation

Документирование соглашений проекта

Крупные команды обычно фиксируют:

  • обязательность providesTags;
  • обязательность invalidatesTags;
  • запрет inline query logic;
  • единый формат ошибок;
  • единый формат params;
  • обязательность transformResponse.

Документирование нестандартных решений

Любое нестандартное поведение должно сопровождаться пояснением.

Плохой пример:

queryFn: async () => {

Без комментариев невозможно понять причину использования queryFn.

Хороший пример:

/**
 * queryFn используется вместо query,
 * поскольку endpoint объединяет
 * несколько последовательных запросов.
 */

Документирование queryFn

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 часто используется вместе с:

  • Swagger;
  • OpenAPI;
  • Redoc;
  • Stoplight.

В таких проектах документация делится на два уровня:

  1. Backend-контракты;
  2. Frontend-реализация RTK Query.

Документирование OpenAPI-интеграции

Пример:

/**
 * 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[]}
 */

Документирование side effects

Mutation endpoints часто содержат side effects:

  • навигацию;
  • уведомления;
  • запись в localStorage;
  • dispatch дополнительных actions.

Подобное поведение должно быть явно описано.

Пример:

/**
 * После успешного логина:
 * - access token сохраняется в store;
 * - refresh token сохраняется в cookie;
 * - пользователь перенаправляется в dashboard.
 */

Документирование ограничений API

Важно фиксировать ограничения:

  • rate limits;
  • размер payload;
  • ограничения файлов;
  • timeout;
  • ограничения сортировки;
  • ограничения фильтрации.

Документирование multipart upload

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

        formData.append('file', file);

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

Описание:

/**
 * Максимальный размер файла: 10MB.
 *
 * Поддерживаемые форматы:
 * - jpg
 * - png
 * - pdf
 */