Именование endpoints и тегов

В RTK Query имена endpoints, тегов, reducer path, хуков и связанных сущностей формируют основу архитектуры слоя работы с серверными данными. Непродуманное именование быстро приводит к проблемам:

  • дублирование запросов;
  • путаница между query и mutation;
  • конфликт тегов;
  • сложность поддержки invalidate-механизмов;
  • ухудшение читаемости API;
  • рост технического долга.

Грамотная система именования позволяет:

  • быстро ориентироваться в API-сервисах;
  • упрощать масштабирование;
  • стандартизировать кодовую базу;
  • уменьшать вероятность ошибок при кэшировании;
  • делать invalidate-политику предсказуемой.

Именование endpoints

Базовые принципы

Название endpoint должно:

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

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

user
data
load
request
item

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

getUsers
getUserById
createUser
updateUser
deleteUser
searchUsers

Разделение query и mutation

Наиболее распространённый стиль:

  • query → начинается с get
  • mutation → начинается с глагола действия

Пример:

endpoints: (builder) => ({
  getUsers: builder.query({
    query: () => '/users',
  }),

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

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

  updateUser: builder.mutation({
    query: ({ id, ...body }) => ({
      url: `/users/${id}`,
      method: 'PUT',
      body,
    }),
  }),

  deleteUser: builder.mutation({
    query: (id) => ({
      url: `/users/${id}`,
      method: 'DELETE',
    }),
  }),
})

Такой подход даёт важные преимущества:

  • мгновенное понимание типа операции;
  • удобный поиск по проекту;
  • предсказуемые имена generated hooks;
  • единый стиль команды.

Предсказуемость generated hooks

RTK Query генерирует хуки автоматически:

getUsers
↓
useGetUsersQuery
createUser
↓
useCreateUserMutation

Если endpoint называется неудачно:

users

то generated hook становится:

useUsersQuery

Название теряет смысл:

  • это список?
  • это один пользователь?
  • это mutation?
  • это поиск?

Поэтому endpoint должен быть максимально явным.


Именование query endpoints

Получение коллекции

Стандарт:

getUsers
getPosts
getProducts

Пример:

getProducts: builder.query({
  query: () => '/products',
})

Получение сущности по идентификатору

Стандарт:

getUserById
getPostById
getProductById

Пример:

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

Получение по slug

getArticleBySlug

Пример:

getArticleBySlug: builder.query({
  query: (slug) => `/articles/${slug}`,
})

Поисковые запросы

Используется префикс:

searchUsers
searchProducts
searchArticles

Пример:

searchProducts: builder.query({
  query: (term) => ({
    url: '/products/search',
    params: { q: term },
  }),
})

Фильтрация

Если endpoint выполняет фильтрацию:

filterProducts
filterOrders

Но чаще предпочтительнее сохранить универсальный endpoint:

getProducts

и передавать фильтры параметрами:

getProducts: builder.query({
  query: (params) => ({
    url: '/products',
    params,
  }),
})

Это уменьшает количество endpoint-ов.


Пагинация

Не рекомендуется:

getProductsPage

Лучше:

getProducts

с параметрами:

useGetProductsQuery({
  page: 1,
  limit: 20,
})

Пагинация — это состояние запроса, а не отдельный тип endpoint.


Именование mutation endpoints

Создание

Стандарт:

createUser
createOrder
createComment

Обновление

Стандарт:

updateUser
updateProfile
updateSettings

Частичное обновление

Если проект разделяет PUT и PATCH:

patchUser
patchSettings

или:

partialUpdateUser

Первый вариант короче и чаще используется.


Удаление

Стандарт:

deleteUser
deleteComment
deleteProduct

Не рекомендуется:

removeUser
destroyUser
eraseUser

Причина — отсутствие единообразия.


Авторизация

Стандартные варианты:

login
logout
refreshToken
register

Пример:

login: builder.mutation({
  query: (credentials) => ({
    url: '/auth/login',
    method: 'POST',
    body: credentials,
  }),
})

Именование специализированных endpoints

Загрузка файлов

uploadAvatar
uploadDocument
uploadImage

Экспорт данных

exportUsers
exportReport

Импорт данных

importProducts
importUsers

Массовые операции

bulkDeleteUsers
bulkUpdateProducts
bulkCreateTags

Префикс bulk сразу показывает пакетную операцию.


Избежание неоднозначности

Плохо:

getData
save
update

Хорошо:

getUserSettings
saveDraftArticle
updateShippingAddress

Чем больше проект — тем важнее конкретика.


Именование API slices

Хороший стиль

userApi
authApi
productApi
orderApi

Пример:

export const userApi = createApi({
  reducerPath: 'userApi',
  endpoints: () => ({}),
})

Именование reducerPath

Обычно совпадает с именем API slice:

reducerPath: 'userApi'

Не рекомендуется:

api
data
store

Причина — возможные конфликты.


Именование тегов

Назначение тегов

Теги используются для:

  • инвалидирования кэша;
  • автоматического refetch;
  • связывания query и mutation.

Пример:

providesTags: ['User']
invalidatesTags: ['User']

Основные правила именования тегов

Тег должен:

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

PascalCase как стандарт

Наиболее распространённый стиль:

'User'
'Post'
'Comment'
'Product'

Преимущества:

  • визуально отличается от endpoint;
  • удобно читать;
  • соответствует стилю entity naming.

Единственное число или множественное

Рекомендуется использовать единственное число:

'User'

а не:

'Users'

Причина — тег описывает тип сущности, а не коллекцию.


Простая система тегов

tagTypes: ['User']
getUsers: builder.query({
  query: () => '/users',
  providesTags: ['User'],
})
createUser: builder.mutation({
  query: (body) => ({
    url: '/users',
    method: 'POST',
    body,
  }),
  invalidatesTags: ['User'],
})

Именование entity tags

Для точечной инвалидации:

providesTags: (result, error, id) => [
  { type: 'User', id },
]

Тег:

{ type: 'User', id: 15 }

означает конкретного пользователя.


LIST-теги

Очень распространённый паттерн:

{ type: 'User', id: 'LIST' }

Пример:

getUsers: builder.query({
  query: () => '/users',
  providesTags: (result) =>
    result
      ? [
          ...result.map(({ id }) => ({
            type: 'User',
            id,
          })),
          { type: 'User', id: 'LIST' },
        ]
      : [{ type: 'User', id: 'LIST' }],
})

Зачем нужен LIST

Это позволяет:

  • отдельно инвалидировать коллекцию;
  • отдельно инвалидировать конкретную сущность;
  • уменьшать лишние refetch;
  • точнее управлять кэшем.

Именование специальных идентификаторов

Наиболее распространённые значения:

'LIST'
'PARTIAL-LIST'

Иногда:

'DETAIL'
'STATS'

Важно соблюдать единый стиль.


Конвенция LIST

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

'LIST'

а не:

'list'
'users'
'all'

Причины:

  • визуально выделяется;
  • считается community standard;
  • легко искать по проекту.

Разделение тегов по доменам

Хороший подход

tagTypes: [
  'User',
  'Post',
  'Comment',
  'Category',
]

Плохой подход

tagTypes: [
  'Data',
  'List',
  'Item',
]

Слишком абстрактные теги делают invalidate непредсказуемым.


Именование тегов для вложенных сущностей

Пример:

'UserPost'
'OrderItem'
'ProductReview'

Именование статистических тегов

Если API имеет отдельные endpoints статистики:

'UserStats'
'SalesStats'
'DashboardStats'

Именование тегов для фильтров

Обычно фильтры не выносятся в отдельные теги.

Плохо:

'ActiveUsers'
'ArchivedPosts'

Лучше:

'User'
'Post'

RTK Query сам разделяет кэш по аргументам query.


Нейминг и cache invalidation

Неудачная схема

providesTags: ['Data']
invalidatesTags: ['Data']

Последствия:

  • лишние refetch;
  • массовая инвалидизация;
  • плохая производительность;
  • непредсказуемость.

Хорошая схема

tagTypes: ['User']
providesTags: (result, error, id) => [
  { type: 'User', id },
]

Такой подход делает invalidate точечным.


Консистентность именования

Главное правило — единая система.

Если выбран стиль:

getUserById
createUser
updateUser
deleteUser

то нельзя смешивать:

fetchUser
removeUser
saveUser

Частые ошибки

Слишком короткие имена

Плохо:

get
list
item

Избыточные имена

Плохо:

getAllUsersListFromServer

Хорошо:

getUsers

Смешивание терминов

Плохо:

createUser
removeUser
patchUser
saveUser

Лучше:

createUser
updateUser
deleteUser

Использование технических терминов вместо бизнес-сущностей

Плохо:

getEntity
getRecord

Хорошо:

getInvoice
getCustomer
getShipment

Domain-driven naming

Крупные проекты часто используют доменное именование.

Пример:

billingApi
inventoryApi
analyticsApi
crmApi

Endpoints:

getInvoices
createInvoice
payInvoice
cancelInvoice

Такой подход особенно полезен в enterprise-системах.


Именование в больших monorepo

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

user/getUsers
auth/login
catalog/getProducts

Но внутри RTK Query чаще достаточно domain API slice:

userApi
catalogApi

Унификация generated hooks

Хорошо структурированный API создаёт предсказуемые хуки:

useGetUsersQuery
useGetUserByIdQuery
useCreateUserMutation
useDeleteUserMutation

Это значительно улучшает DX.


Рекомендуемая конвенция

Query endpoints

getUsers
getUserById
searchUsers

Mutation endpoints

createUser
updateUser
deleteUser

API slices

userApi
authApi
productApi

Tag types

'User'
'Product'
'Order'

Collection tags

{ type: 'User', id: 'LIST' }

Пример полноценной структуры

export const userApi = createApi({
  reducerPath: 'userApi',

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

  tagTypes: ['User'],

  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => '/users',

      providesTags: (result) =>
        result
          ? [
              ...result.map(({ id }) => ({
                type: 'User',
                id,
              })),
              { type: 'User', id: 'LIST' },
            ]
          : [{ type: 'User', id: 'LIST' }],
    }),

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

      providesTags: (result, error, id) => [
        { type: 'User', id },
      ],
    }),

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

      invalidatesTags: [
        { type: 'User', id: 'LIST' },
      ],
    }),

    updateUser: builder.mutation({
      query: ({ id, ...body }) => ({
        url: `/users/${id}`,
        method: 'PUT',
        body,
      }),

      invalidatesTags: (result, error, { id }) => [
        { type: 'User', id },
      ],
    }),

    deleteUser: builder.mutation({
      query: (id) => ({
        url: `/users/${id}`,
        method: 'DELETE',
      }),

      invalidatesTags: [
        { type: 'User', id: 'LIST' },
      ],
    }),
  }),
})