Files

25 KiB
Raw Permalink Blame History

name, description, metadata
name description metadata
rest-client Используй при создании, изменении или ревью REST API клиента и infra REST-модуля. Триггеры: REST API, REST client, API client, backend API, external API, OpenAPI, Swagger, ручной клиент без OpenAPI, @gromlab/api-codegen, @gromlab/api-codegen@5.1.0, src/infra/*-rest-api, *-rest-api-sdk, REST SDK, npm SDK, client.ts, rest-api.ts, operations-tree.ts, *HttpClient, *RestApi, generated/, operations/, operations/*, data-contracts, hooks/, types/, errors/, HttpClient, ApiRequestClient, RequestParams, ContentType, createApiClient, operationsTree, full client, minimal client, SDK exports, onRequest, onResponse, onError, JWT, refresh token, ApiError, useGet*, get*Key, useSWR, SWRConfiguration, DTO, API error, public API REST-модуля. НЕ используй для любого fetch вне REST-клиента проекта, route-level data fetching Next.js, SLM-архитектуры без REST-модуля, code style, SVG sprites или генерации шаблонов.
internal
true

REST Client

Работа с REST-клиентом

Базовые Правила

  • REST-клиент сервиса оформляй как отдельный infra/ module с именем {name}-rest-api: pet-store-rest-api, billing-rest-api, maps-rest-api.
  • Генерация внутри клиента живёт в {name}-rest-api/generated. Generated-клиент, выносимый в npm-пакет или пакет монорепозитория, называется {name}-rest-api-sdk.
  • Внешний код импортирует клиент, типы, enum и GET-хуки только через корневой index.ts REST-модуля.
  • client.ts экспортирует только настроенный транспортный *HttpClient: petStoreHttpClient, billingHttpClient, mapsHttpClient.
  • rest-api.ts экспортирует полный bound API-клиент *RestApi, собранный через createApiClient(*HttpClient, operationsTree).
  • Не размещай в client.ts DTO, declare module, Extended-типы, GET-хуки и бизнес-логику.
  • GET-хуки не импортируют полный *RestApi; они импортируют точечную operation и вызывают её через общий *HttpClient.
  • hooks/index.ts содержит 'use client'; отдельные use-get-*.hook.ts остаются обычными файлами без директивы.
  • Если OpenAPI нет, ручные operations пиши через @gromlab/api-codegen@5.1.0+ с тем же контрактом, что у generated SDK: operation(http, params/body, requestParams?).
  • Не создавай собственные fetch-классы, methods/*Methods(client) и ручные get/post wrappers для нового ручного клиента.
  • Не правь файлы в generated/ руками. Расширения типов, DTO и именованные response-типы держи в types/ REST-модуля.
  • UI/components не импортируют SDK-пакет, generated/, operations/, operationsTree и транспорт напрямую.
  • SDK operations напрямую импортируются только внутри infra/*-rest-api и boundary-файлов минимальных clients в compositions или features.
  • Для прямого REST-вызова в server code, submit-функции или сервисе используй именованный API-объект клиента из публичного API REST-модуля или минимальный composition client.
  • Для GET-данных в Client Component сначала используй готовый useGet* hook REST-модуля.
  • GET-хуки являются прозрачными SWR-обёртками над GET operation и живут в hooks/ этого REST-модуля.
  • Не выноси SWR keys, fetcher, DTO mapping, API errors и транспортные детали в UI-компоненты.
  • Если в коде появляется бизнес-смысл вроде isAuth, canEdit, hasAccess или hasPets, это уже не REST-клиент, а business/.

Full Client vs Minimal Client

  • Full client живёт только в infra/{name}-rest-api/rest-api.ts.
  • Full client собирается из всего generated дерева: createApiClient(nameHttpClient, operationsTree).
  • Full client экспортируется через infra/{name}-rest-api/index.ts как nameRestApi.
  • Minimal client допустим только в feature/composition boundary-файле, где нужен ограниченный набор операций для конкретного бизнес-сценария.
  • Minimal client собирается из selected operations: createApiClient(nameHttpClient, { ...только нужные операции... }).
  • GET-hook не использует bound client вообще: он вызывает operation(nameHttpClient, params, requestParams?).
  • operationsTree не импортируется в GET-хуки и minimal clients.
  • Для ручного клиента operationsTree пишется вручную в infra/{name}-rest-api/operations-tree.ts.

Рабочий Алгоритм

  1. Найди REST-модуль сервиса в infra/ и работай через его публичный API.
  2. Проверь корневой index.ts: внешний код не должен импортировать из generated/, hooks/, types/ или errors/ напрямую.
  3. Если нужен прямой REST-вызов в server code, submit-функции или сервисе, используй полный *RestApi из infra/{name}-rest-api.
  4. Если данные нужны в Client Component и запрос является GET, сначала ищи готовый useGet* hook.
  5. Если GET-хука нет, добавь его рядом с клиентом в hooks/ по контракту раздела GET-хуки REST-клиента.
  6. Если feature/composition нужен компактный клиент с небольшим набором операций, создай minimal client в boundary-файле этой composition.
  7. Если нужно изменить тип ответа или дополнить generated-тип, не меняй generated-файл; добавь тип или расширение в types/.
  8. Если REST-клиента ещё нет или нужно подключить новый внешний API, открой конкретный локальный setup-материал ниже.
  9. После изменений проверь публичные экспорты, отсутствие SDK imports в UI/components и то, что SWR-механика не утекла в компоненты.

Создание И Настройка Клиента

Создание нового REST-клиента - редкий сценарий. Не открывай setup-материалы, если задача сводится к использованию существующего клиента, GET-хука или публичного API.

Включённые Разделы

Использование REST-клиента

Как выбрать правильную точку вызова REST API.

Прямой вызов полного API

Для server code, submit-функции, adapter или сервиса импортируйте полный API-клиент из публичного API REST-модуля.

import { petStoreRestApi } from 'infra/pet-store-rest-api'

export const getPet = async (petId: number) => {
  return petStoreRestApi.pet.getPetById({ petId })
}

Внешний код не импортирует SDK-пакет, generated/, operations/, operationsTree, client.ts или rest-api.ts напрямую.

Minimal Client В Composition Boundary

Если business composition нужен компактный клиент из нескольких операций, собирайте его в boundary-файле этой composition.

// src/compositions/business/pet-store/orders/pet-store-rest-api.ts
import { createApiClient } from '@company/pet-store-rest-api-sdk/create-api-client'
import { createOrder } from '@company/pet-store-rest-api-sdk/operations/create-order'
import { getOrder } from '@company/pet-store-rest-api-sdk/operations/get-order'
import { petStoreHttpClient } from 'infra/pet-store-rest-api'

export const petStoreOrdersRestApi = createApiClient(petStoreHttpClient, {
  orders: {
    create: createOrder,
    get: getOrder,
  },
})

Minimal client не экспортируется из общего infra/{name}-rest-api. Он принадлежит конкретному feature/composition boundary и содержит только операции этого сценария.

GET В Client Components

Client Components используют только готовые useGet* hooks REST-модуля.

import { useGetPetDetail } from 'infra/pet-store-rest-api'

export const PetCard = ({ petId }: { petId: number }) => {
  const { data: pet } = useGetPetDetail({ petId })

  return <div>{pet?.name}</div>
}

Не вызывайте useSWR, SDK operation или полный *RestApi прямо в UI-компоненте.

GET-хуки REST-клиента

Прозрачные SWR-обёртки над GET operations REST-клиента.

Зачем нужны

GET-хуки нужны, чтобы Client Components получали REST-данные через SWR, но не работали с useSWR, ключами кеша и fetcher напрямую.

Где лежат

GET-хуки принадлежат REST-клиенту конкретного сервиса и живут рядом с ним:

src/infra/
└── pet-store-rest-api/
    ├── client.ts
    ├── rest-api.ts
    ├── generated/
    ├── hooks/
    │   ├── lib/
    │   │   └── create-query-string.ts
    │   ├── use-get-pet-list.hook.ts
    │   ├── use-get-pet-detail.hook.ts
    │   └── index.ts
    ├── types/
    └── index.ts

Контракт

  • Один GET-хук = одна GET operation.
  • Имя GET-хука начинается с useGet: useGetPetList, useGetPetDetail.
  • Имя файла начинается с use-get: use-get-pet-list.hook.ts.
  • Хук принимает params?: GeneratedParams | null и config?: SWRConfiguration<Data>.
  • Для GET operation без параметров хук принимает только config?: SWRConfiguration<Data>.
  • Key-функция принимает те же params, что и хук.
  • Key-функция возвращает null, если обязательные параметры не готовы.
  • Проверка готовности запроса живёт в key-функции, а не в теле хука.
  • Хук вызывает useSWR один раз и безусловно.
  • Fetcher вызывает точечную operation через общий *HttpClient: operation(nameHttpClient, params, requestParams?).
  • Fetcher не проверяет null, не бросает ошибку и не вызывает operation с null.
  • Внутри только SWR-механика: key, fetcher, useSWR, config.
  • Хук возвращает тип ответа API: generated-тип или DTO из types/.
  • Хук не объединяет несколько запросов.
  • Хук не маппит DTO в доменную модель.
  • Хук не вычисляет бизнес-флаги: isAuth, canEdit, hasAccess, hasPets.
  • Хук не вызывает тосты, модалки, редиректы и не пишет UI-состояние.
  • Хук не импортирует полный *RestApi, createApiClient или operationsTree.
  • hooks/index.ts содержит 'use client'; отдельные use-get-*.hook.ts не содержат эту директиву.

Формат SWR-ключа

SWR-ключ GET-хука всегда создаётся отдельной экспортируемой функцией.

Формат ключа:

['pet-store-rest-api', '/pet/10'] as const
  • Первый элемент — имя API-сервиса или REST-клиента в kebab-case.
  • Второй элемент — endpoint запроса: path и query string.
  • Key-функция возвращает null, когда запрос нельзя выполнять.
  • Key-функция нужна и GET-хуку, и SWRConfig fallback.
  • Не используйте произвольные части вроде ['pet-store-rest-api', 'pet', 'detail', params].
  • Не используйте только строку endpoint без имени сервиса.

Примеры ключей:

export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
  if (!params?.petId) {
    return null
  }

  return ['pet-store-rest-api', `/pet/${params.petId}`] as const
}
export const getPetListKey = (params?: FindPetsByStatusParams | null) => {
  if (!params?.status) {
    return null
  }

  return ['pet-store-rest-api', `/pet/findByStatus?status=${params.status}`] as const
}
export const getPetListByTagsKey = (params?: FindPetsByTagsParams | null) => {
  if (!params?.tags.length) {
    return null
  }

  return ['pet-store-rest-api', `/pet/findByTags?tags=${params.tags.join(',')}`] as const
}

Если API допускает 0 как валидный идентификатор, не используйте проверку !params?.id. В таком случае проверяйте null и undefined явно.

Query String Для Key

Если key зависит от query-параметров, собирайте query string отдельной маленькой функцией или общим helper внутри hooks/lib/.

// src/infra/pet-store-rest-api/hooks/lib/create-query-string.ts
type QueryValue = boolean | number | string | null | undefined

export const createQueryString = (query: Record<string, QueryValue>): string => {
  const searchParams = new URLSearchParams()

  Object.entries(query).forEach(([key, value]) => {
    if (value === null || value === undefined || value === '') {
      return
    }

    searchParams.set(key, String(value))
  })

  const search = searchParams.toString()

  return search ? `?${search}` : ''
}

Key должен отражать фактический URL запроса: path плюс query string. Не кладите весь params object в SWR key.

Пример списка

// src/infra/pet-store-rest-api/hooks/use-get-pet-list.hook.ts
import { findPetsByStatus } from '../generated/operations/find-pets-by-status'
import type { SWRConfiguration } from 'swr'
import useSWR from 'swr'
import { petStoreHttpClient } from '../client'
import { createQueryString } from './lib/create-query-string'
import type { FindPetsByStatusParams, Pet } from '../generated'

const getPetListQuery = (params: FindPetsByStatusParams): string => {
  return createQueryString({ status: params.status })
}

export const getPetListKey = (params?: FindPetsByStatusParams | null) => {
  if (!params?.status) {
    return null
  }

  return ['pet-store-rest-api', `/pet/findByStatus${getPetListQuery(params)}`] as const
}

/**
 * Получает список питомцев по статусу.
 */
export const useGetPetList = (
  params?: FindPetsByStatusParams | null,
  config?: SWRConfiguration<Pet[]>,
) => {
  const key = getPetListKey(params)
  const fetcher = () => findPetsByStatus(
    petStoreHttpClient,
    params as FindPetsByStatusParams,
  )

  return useSWR<Pet[]>(key, fetcher, config)
}

params as FindPetsByStatusParams допустим только в fetcher: готовность параметров проверена в key-функции, а при key = null SWR не вызывает fetcher.

Пример detail-запроса

// src/infra/pet-store-rest-api/hooks/use-get-pet-detail.hook.ts
import { getPetById } from '../generated/operations/get-pet-by-id'
import type { SWRConfiguration } from 'swr'
import useSWR from 'swr'
import { petStoreHttpClient } from '../client'
import type { GetPetByIdParams, Pet } from '../generated'

export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
  if (!params?.petId) {
    return null
  }

  return ['pet-store-rest-api', `/pet/${params.petId}`] as const
}

/**
 * Получает детальную карточку питомца с кешированием результата.
 */
export const useGetPetDetail = (
  params?: GetPetByIdParams | null,
  config?: SWRConfiguration<Pet>,
) => {
  const key = getPetDetailKey(params)
  const fetcher = () => getPetById(petStoreHttpClient, params as GetPetByIdParams)

  return useSWR<Pet>(key, fetcher, config)
}

Пример без параметров

// src/infra/pet-store-rest-api/hooks/use-get-store-inventory.hook.ts
import { getStoreInventory } from '../generated/operations/get-store-inventory'
import type { SWRConfiguration } from 'swr'
import useSWR from 'swr'
import { petStoreHttpClient } from '../client'
import type { StoreInventory } from '../types'

export const getStoreInventoryKey = () => {
  return ['pet-store-rest-api', '/store/inventory'] as const
}

/**
 * Получает инвентарь магазина.
 */
export const useGetStoreInventory = (
  config?: SWRConfiguration<StoreInventory>,
) => {
  return useSWR<StoreInventory>(
    getStoreInventoryKey(),
    () => getStoreInventory(petStoreHttpClient),
    config,
  )
}

Если generated operation возвращает безымянный тип вроде Record<string, number>, а тип нужен наружу, вынесите его в types/.

Пример С Request Params

Если operation зависит от разовых headers или дополнительных query-параметров, соберите requestParams внутри fetcher. Key-функция должна учитывать параметры, которые меняют результат запроса.

// src/infra/cms-rest-api/hooks/use-get-post-detail.hook.ts
import { postsDetail } from '@company/cms-rest-api-sdk/operations/posts-detail'
import type { SWRConfiguration } from 'swr'
import useSWR from 'swr'
import { cmsHttpClient } from '../client'
import { createQueryString } from './lib/create-query-string'
import type {
  PostDetail,
  PostsDetailParams,
  RequestParams,
} from '@company/cms-rest-api-sdk'

export type GetPostDetailParams = PostsDetailParams & {
  app?: string
}

const getPostDetailPath = (params: GetPostDetailParams): string => {
  return `/v1/posts/${params.slug}${createQueryString({
    app: params.app,
    status: params.status,
  })}`
}

export const getPostDetailKey = (params?: GetPostDetailParams | null) => {
  if (!params?.slug) {
    return null
  }

  return ['cms-rest-api', getPostDetailPath(params)] as const
}

export const useGetPostDetail = (
  params?: GetPostDetailParams | null,
  config?: SWRConfiguration<PostDetail>,
) => {
  const key = getPostDetailKey(params)
  const fetcher = () => {
    const { app, ...postParams } = params as GetPostDetailParams
    const requestParams: RequestParams = {
      headers: app ? { 'x-app': app } : undefined,
    }

    return postsDetail(cmsHttpClient, postParams, requestParams)
  }

  return useSWR<PostDetail>(key, fetcher, config)
}

Отложенный запрос

GET-хук может принимать null или undefined для обязательных параметров. Это означает, что параметры ещё не готовы и запрос выполнять нельзя.

const key = getPetDetailKey(params)

Если params не готов, key-функция вернёт null. SWR не вызовет fetcher для null-ключа.

Не добавляйте отдельные isReady, throw new Error(...) и условный вызов useSWR.

Экспорт

// src/infra/pet-store-rest-api/hooks/index.ts
'use client'

export { getPetListKey, useGetPetList } from './use-get-pet-list.hook'
export { getPetDetailKey, useGetPetDetail } from './use-get-pet-detail.hook'
export {
  getStoreInventoryKey,
  useGetStoreInventory,
} from './use-get-store-inventory.hook'
// src/infra/pet-store-rest-api/index.ts
export { petStoreHttpClient } from './client'
export { petStoreRestApi } from './rest-api'
export type {
  FindPetsByStatusParams,
  GetPetByIdParams,
  Pet,
} from './generated'
export * from './hooks'
export type { StoreInventory } from './types'

Наружу импортируют только из infra/pet-store-rest-api, не из generated/ и не из hooks/ напрямую.

Где заканчивается infra

// Хорошо: infra, прозрачный GET-хук
const { data: pets } = useGetPetList({ status: 'available' })
// Хорошо: business, доменная интерпретация
export const useAvailablePets = () => {
  const query = useGetPetList({ status: 'available' })

  return {
    ...query,
    hasPets: Boolean(query.data?.length),
  }
}

hasPets — не часть GET-запроса, поэтому он не добавляется в useGetPetList.

Что запрещено

// Плохо — useSWR в компоненте
const { data } = useSWR(
  ['pet-store-rest-api', '/pet/findByStatus?status=available'],
  () => findPetsByStatus(petStoreHttpClient, { status: 'available' }),
)

// Плохо — проверка готовности размазана по хуку
export const useGetPetDetail = (params?: GetPetByIdParams | null) => {
  const key = params?.petId ? getPetDetailKey(params) : null
  const fetcher = () => {
    if (!params?.petId) {
      throw new Error('Pet id is required')
    }

    return getPetById(petStoreHttpClient, params)
  }

  return useSWR<Pet>(key, fetcher)
}

// Плохо — условный вызов useSWR нарушает rules of hooks
export const useGetPetDetail = (params?: GetPetByIdParams | null) => {
  const key = getPetDetailKey(params)

  if (key === null) {
    return useSWR(null, null)
  }

  return useSWR(key, () => getPetById(petStoreHttpClient, params))
}

// Плохо — GET-хук импортирует полный bound client вместо точечной operation
export const useGetPetDetail = (params?: GetPetByIdParams | null) => {
  const key = getPetDetailKey(params)

  return useSWR(
    key,
    () => petStoreRestApi.pet.getPetById(params as GetPetByIdParams),
  )
}

// Плохо — несколько GET внутри infra-хука
export const usePetDashboard = () => {
  const available = useGetPetList({ status: 'available' })
  const sold = useGetPetList({ status: 'sold' })

  return { available, sold }
}

// Плохо — бизнес-флаг внутри GET-хука REST-клиента
export const useGetPetList = (params?: FindPetsByStatusParams | null) => {
  const query = useSWR(...)

  return {
    ...query,
    hasPets: Boolean(query.data?.length),
  }
}

Потребление таких хуков на уровне route-level data fetching относится к nextjs-style-guide.