mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 07:30:16 +03:00
540 lines
25 KiB
Markdown
540 lines
25 KiB
Markdown
---
|
||
name: rest-client
|
||
description: "Используй при создании, изменении или ревью 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 или генерации шаблонов."
|
||
metadata:
|
||
internal: true
|
||
---
|
||
|
||
<!-- Generated from src/SKILL.md. Do not edit manually. -->
|
||
|
||
# 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-клиента](./reference/canons/setup.md) - состав REST-клиента, структура модуля и базовая настройка.
|
||
- [Автогенерация из OpenAPI](./reference/canons/auto.md) - генерация split-клиента через `@gromlab/api-codegen`.
|
||
- [Кастомизация HTTP-клиента](./reference/canons/http-client.md) - опции и хуки `HttpClient`: авторизация, refresh token, транспорт.
|
||
- [SDK-пакет REST-клиента](./reference/canons/sdk.md) - вынос generated-клиента в npm-пакет или пакет монорепозитория `{name}-rest-api-sdk`.
|
||
- [Ручное создание](./reference/canons/manual.md) - ручной REST-клиент, если OpenAPI нет или он неполный.
|
||
|
||
### Включённые Разделы
|
||
|
||
- [Использование REST-клиента](#использование-rest-клиента) - прямой вызов готового клиента.
|
||
- [GET-хуки REST-клиента](#get-хуки-rest-клиента) - контракт `useGet*`, key-функций и SWR-обёрток.
|
||
|
||
## Использование REST-клиента
|
||
|
||
Как выбрать правильную точку вызова REST API.
|
||
|
||
### Прямой вызов полного API
|
||
|
||
Для server code, submit-функции, adapter или сервиса импортируйте полный API-клиент из публичного API REST-модуля.
|
||
|
||
```ts
|
||
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.
|
||
|
||
```ts
|
||
// 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-модуля.
|
||
|
||
```tsx
|
||
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-клиенту конкретного сервиса и живут рядом с ним:
|
||
|
||
```text
|
||
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-хука всегда создаётся отдельной экспортируемой функцией.
|
||
|
||
Формат ключа:
|
||
|
||
```ts
|
||
['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 без имени сервиса.
|
||
|
||
Примеры ключей:
|
||
|
||
```ts
|
||
export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
|
||
if (!params?.petId) {
|
||
return null
|
||
}
|
||
|
||
return ['pet-store-rest-api', `/pet/${params.petId}`] as const
|
||
}
|
||
```
|
||
|
||
```ts
|
||
export const getPetListKey = (params?: FindPetsByStatusParams | null) => {
|
||
if (!params?.status) {
|
||
return null
|
||
}
|
||
|
||
return ['pet-store-rest-api', `/pet/findByStatus?status=${params.status}`] as const
|
||
}
|
||
```
|
||
|
||
```ts
|
||
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/`.
|
||
|
||
```ts
|
||
// 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.
|
||
|
||
### Пример списка
|
||
|
||
```ts
|
||
// 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-запроса
|
||
|
||
```ts
|
||
// 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)
|
||
}
|
||
```
|
||
|
||
### Пример без параметров
|
||
|
||
```ts
|
||
// 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-функция должна учитывать параметры, которые меняют результат запроса.
|
||
|
||
```ts
|
||
// 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` для обязательных параметров. Это означает, что параметры ещё не готовы и запрос выполнять нельзя.
|
||
|
||
```ts
|
||
const key = getPetDetailKey(params)
|
||
```
|
||
|
||
Если `params` не готов, key-функция вернёт `null`. SWR не вызовет fetcher для `null`-ключа.
|
||
|
||
Не добавляйте отдельные `isReady`, `throw new Error(...)` и условный вызов `useSWR`.
|
||
|
||
### Экспорт
|
||
|
||
```ts
|
||
// 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'
|
||
```
|
||
|
||
```ts
|
||
// 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
|
||
|
||
```ts
|
||
// Хорошо: infra, прозрачный GET-хук
|
||
const { data: pets } = useGetPetList({ status: 'available' })
|
||
```
|
||
|
||
```ts
|
||
// Хорошо: business, доменная интерпретация
|
||
export const useAvailablePets = () => {
|
||
const query = useGetPetList({ status: 'available' })
|
||
|
||
return {
|
||
...query,
|
||
hasPets: Boolean(query.data?.length),
|
||
}
|
||
}
|
||
```
|
||
|
||
`hasPets` — не часть GET-запроса, поэтому он не добавляется в `useGetPetList`.
|
||
|
||
### Что запрещено
|
||
|
||
```ts
|
||
// Плохо — 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`.
|