mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 07:30:16 +03:00
feat: add example
This commit is contained in:
537
.opencode/skills/rest-client/SKILL.md
Normal file
537
.opencode/skills/rest-client/SKILL.md
Normal file
@@ -0,0 +1,537 @@
|
||||
---
|
||||
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 или генерации шаблонов."
|
||||
---
|
||||
|
||||
<!-- 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`.
|
||||
4
.opencode/skills/rest-client/agents/openai.yaml
Normal file
4
.opencode/skills/rest-client/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "REST Client"
|
||||
short_description: "Generated и manual REST-клиенты проекта"
|
||||
default_prompt: "Use $rest-client to generate, update, or review REST clients and their integration points in the monorepo."
|
||||
342
.opencode/skills/rest-client/reference/canons/auto.md
Normal file
342
.opencode/skills/rest-client/reference/canons/auto.md
Normal file
@@ -0,0 +1,342 @@
|
||||
---
|
||||
title: Автогенерация REST-клиента
|
||||
description: Генерация split REST-клиента из OpenAPI-спецификации.
|
||||
keywords: [rest, openapi, api-codegen, автогенерация, generated, split, operations, npx]
|
||||
---
|
||||
|
||||
# Автогенерация REST-клиента
|
||||
|
||||
Генерация REST-клиента из OpenAPI-спецификации.
|
||||
|
||||
## Когда использовать
|
||||
|
||||
Автогенерация используется, когда у API есть актуальная OpenAPI-спецификация. Генератор создаёт split-клиент: HTTP-клиент, типы и отдельную operation-функцию на каждый endpoint. Разработчик вручную добавляет транспорт в `client.ts`, полный API в `rest-api.ts` и GET-хуки.
|
||||
|
||||
По умолчанию генерация идёт внутрь infra-модуля приложения в `{name}-rest-api/generated` — этот сценарий описан ниже. Если клиент нужен нескольким приложениям, generated-код выносится в отдельный пакет `{name}-rest-api-sdk`: [SDK-пакет REST-клиента](./sdk.md).
|
||||
|
||||
## Пример API
|
||||
|
||||
В примерах используется Swagger Petstore:
|
||||
|
||||
```text
|
||||
https://petstore3.swagger.io/api/v3/openapi.json
|
||||
```
|
||||
|
||||
Имена модуля:
|
||||
|
||||
```text
|
||||
src/infra/pet-store-rest-api/
|
||||
petStoreRestApi
|
||||
```
|
||||
|
||||
## Скрипт генерации
|
||||
|
||||
`@gromlab/api-codegen` не устанавливается в `devDependencies`. Используем `npx @gromlab/api-codegen@latest`, чтобы запускать свежую версию.
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"codegen:pet-store-rest-api": "npx @gromlab/api-codegen@latest -i https://petstore3.swagger.io/api/v3/openapi.json -o src/infra/pet-store-rest-api/generated"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Параметры:
|
||||
|
||||
- `-i` — путь к OpenAPI-спецификации: URL или локальный файл.
|
||||
- `-o` — директория для сгенерированного split-клиента.
|
||||
|
||||
По умолчанию генератор работает в режиме `split`. Legacy-режим `--mode single -n <имя>` генерирует один монолитный файл и используется только в старых проектах, которые уже завязаны на монолитный generated-клиент. В новых проектах single-режим не используется.
|
||||
|
||||
Генератор не создаёт SWR-хуки. GET-хуки REST-клиента пишутся вручную, чтобы сохранить проектный контракт: один GET-хук = одна GET operation, без бизнес-логики и композиции.
|
||||
|
||||
## Генерация
|
||||
|
||||
```bash
|
||||
npm run codegen:pet-store-rest-api
|
||||
```
|
||||
|
||||
Ожидаемый результат:
|
||||
|
||||
```text
|
||||
src/infra/pet-store-rest-api/generated/
|
||||
├── create-api-client.ts
|
||||
├── data-contracts.ts
|
||||
├── http-client.ts
|
||||
├── index.ts
|
||||
├── operations-tree.ts
|
||||
└── operations/
|
||||
├── index.ts
|
||||
├── get-pet-by-id.ts
|
||||
└── find-pets-by-status.ts
|
||||
```
|
||||
|
||||
Основные части:
|
||||
|
||||
- `http-client.ts` — fetch-based `HttpClient`: `baseUrl`, заголовки, авторизация и хуки транспорта.
|
||||
- `data-contracts.ts` — TypeScript-типы из OpenAPI schemas, включая `*Params`-типы операций.
|
||||
- `operations/*.ts` — отдельная typed operation-функция на каждый endpoint.
|
||||
- `operations-tree.ts` — дерево всех операций для сборки полного клиента.
|
||||
- `create-api-client.ts` — `createApiClient`, который привязывает дерево операций к `HttpClient`.
|
||||
- `index.ts` — входная точка generated-кода, реэкспортирует всё перечисленное.
|
||||
|
||||
Файлы в `generated/` не правятся руками и коммитятся в репозиторий.
|
||||
|
||||
Enum-значения в split-режиме генерируются как union-типы строковых литералов. Отдельных runtime enum в `generated/` нет: в коде используются строковые литералы вроде `'available'`, а тип проверяет их допустимость.
|
||||
|
||||
## Проверка операций
|
||||
|
||||
После генерации откройте `generated/operations-tree.ts` и проверьте фактические имена operation-функций и структуру дерева.
|
||||
|
||||
Для Petstore нужны GET-операции вида:
|
||||
|
||||
```ts
|
||||
import { findPetsByStatus } from './generated/operations/find-pets-by-status'
|
||||
import { getPetById } from './generated/operations/get-pet-by-id'
|
||||
```
|
||||
|
||||
Точечная operation вызывается через настроенный `HttpClient`:
|
||||
|
||||
```ts
|
||||
getPetById(petStoreHttpClient, { petId: 10 })
|
||||
findPetsByStatus(petStoreHttpClient, { status: 'available' })
|
||||
```
|
||||
|
||||
После сборки полного клиента в `rest-api.ts` те же операции доступны как bound API:
|
||||
|
||||
```ts
|
||||
petStoreRestApi.pet.findPetsByStatus({ status: 'available' })
|
||||
petStoreRestApi.pet.getPetById({ petId: 10 })
|
||||
```
|
||||
|
||||
Имена операций и группировка дерева зависят от `operationId` и тегов OpenAPI-схемы. В рабочих задачах всегда сверяйтесь с `generated/operations-tree.ts` и `generated/data-contracts.ts`.
|
||||
|
||||
## Источник Imports
|
||||
|
||||
Если generated-код лежит внутри infra-модуля, импортируйте operation из локального `generated/`:
|
||||
|
||||
```ts
|
||||
import { getPetById } from '../generated/operations/get-pet-by-id'
|
||||
```
|
||||
|
||||
Если generated-код вынесен в SDK-пакет, импортируйте operation через subpath export пакета:
|
||||
|
||||
```ts
|
||||
import { getPetById } from '@company/pet-store-rest-api-sdk/operations/get-pet-by-id'
|
||||
```
|
||||
|
||||
Не импортируйте весь `operations` namespace ради одной операции.
|
||||
|
||||
## Алгоритм для агента
|
||||
|
||||
После генерации агент должен действовать по шагам:
|
||||
|
||||
1. Открыть `generated/operations-tree.ts` и найти фактические имена нужных operation-функций.
|
||||
2. Для каждой нужной операции найти тип параметров и тип ответа в `generated/data-contracts.ts`.
|
||||
3. Создать или обновить `client.ts`: настроить и экспортировать только `*HttpClient`.
|
||||
4. Создать или обновить `rest-api.ts`: собрать полный `*RestApi` через `createApiClient(*HttpClient, operationsTree)`.
|
||||
5. Создать GET-хуки только для реально нужных GET-операций, не для всех операций API на всякий случай.
|
||||
6. В каждом GET-хуке импортировать точечную operation из `operations/<operation-file>`.
|
||||
7. Для каждого GET-хука создать key-функцию формата `[serviceName, endpoint]`.
|
||||
8. В key-функции вернуть `null`, если обязательные параметры не готовы.
|
||||
9. В хуке принять `params?: GeneratedParams | null` и `config?: SWRConfiguration<Data>`.
|
||||
10. В fetcher вызвать operation через общий `*HttpClient`: `operation(nameHttpClient, params as GeneratedParams, requestParams?)`.
|
||||
11. Экспортировать хук и key-функцию из `hooks/index.ts`; если это первый hook, добавить в `hooks/index.ts` директиву `'use client'`.
|
||||
12. Экспортировать наружу только нужные generated-типы, DTO, `*HttpClient`, `*RestApi` и `hooks` через корневой `index.ts`.
|
||||
|
||||
Что агент не должен делать:
|
||||
|
||||
- Не править файлы в `generated/` руками.
|
||||
- Не импортировать операции и `HttpClient` из `generated/` или SDK в UI/components.
|
||||
- Не импортировать `operationsTree` или полный `*RestApi` в GET-хуки.
|
||||
- Не добавлять GET-хуки для POST, PUT, PATCH, DELETE.
|
||||
- Не добавлять бизнес-флаги, тосты, редиректы и UI-состояние в GET-хук.
|
||||
- Не создавать словари enum-маппинга внутри GET-хука.
|
||||
- Не объявлять DTO и response-типы в файле хука.
|
||||
- Не вызывать `useSWR` условно.
|
||||
- Не добавлять `throw` в fetcher для неготовых params.
|
||||
|
||||
## `client.ts`
|
||||
|
||||
`client.ts` содержит только настроенный транспортный `HttpClient`.
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-rest-api/client.ts
|
||||
import { HttpClient } from './generated'
|
||||
|
||||
export const petStoreHttpClient = new HttpClient({
|
||||
baseUrl: 'https://example.com/api',
|
||||
})
|
||||
```
|
||||
|
||||
`client.ts` не содержит расширения типов, `declare module`, `Extended`-типы, GET-хуки, `operationsTree`, `createApiClient` и бизнес-логику.
|
||||
|
||||
Авторизация, refresh token, логирование и другие настройки транспорта задаются опциями и хуками `HttpClient` в этом же файле: [Кастомизация HTTP-клиента](./http-client.md).
|
||||
|
||||
## `rest-api.ts`
|
||||
|
||||
`rest-api.ts` собирает полный bound API-клиент из всего generated дерева операций.
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-rest-api/rest-api.ts
|
||||
import { createApiClient, operationsTree } from './generated'
|
||||
import { petStoreHttpClient } from './client'
|
||||
|
||||
export const petStoreRestApi = createApiClient(petStoreHttpClient, operationsTree)
|
||||
```
|
||||
|
||||
Импорт `operationsTree` означает полный клиент: в bound API доступны все операции API.
|
||||
|
||||
Если generated-код вынесен в SDK-пакет, используйте subpath exports:
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-rest-api/rest-api.ts
|
||||
import { createApiClient } from '@company/pet-store-rest-api-sdk/create-api-client'
|
||||
import { operationsTree } from '@company/pet-store-rest-api-sdk/operations-tree'
|
||||
import { petStoreHttpClient } from './client'
|
||||
|
||||
export const petStoreRestApi = createApiClient(petStoreHttpClient, operationsTree)
|
||||
```
|
||||
|
||||
## GET-хуки
|
||||
|
||||
GET-хуки пишутся вручную после проверки generated-операций.
|
||||
|
||||
Пример для операции `getPetById`:
|
||||
|
||||
```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)
|
||||
}
|
||||
```
|
||||
|
||||
Типы импортируются как `import type` из `./generated`: generated `index.ts` реэкспортирует все типы из `data-contracts.ts`.
|
||||
|
||||
Подробный контракт key-функций, `params`, `config` и запретов описан в разделе [GET-хуки REST-клиента](../../SKILL.md#get-хуки-rest-клиента).
|
||||
|
||||
## Расширение сгенерированных типов
|
||||
|
||||
Файлы в `generated/` не правятся руками. Если OpenAPI-спецификация неполная или генератор дал слишком общий тип (`object`, `unknown`, отсутствующее поле), расширения живут в `types/`.
|
||||
|
||||
```text
|
||||
src/infra/biocad-less-rest-api/
|
||||
├── generated/
|
||||
│ ├── data-contracts.ts
|
||||
│ ├── operations/
|
||||
│ └── index.ts
|
||||
├── types/
|
||||
│ ├── term.ts
|
||||
│ └── index.ts
|
||||
├── client.ts
|
||||
├── rest-api.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Пример расширения generated-типа:
|
||||
|
||||
```ts
|
||||
// src/infra/biocad-less-rest-api/types/term.ts
|
||||
import type { TermRecordItem } from '../generated/data-contracts'
|
||||
|
||||
declare module '../generated/data-contracts' {
|
||||
interface TermRecordItem {
|
||||
media?: {
|
||||
file?: string
|
||||
title?: string
|
||||
url?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type TermRecordItemExtended = Omit<
|
||||
TermRecordItem,
|
||||
'categories' | 'tags' | 'fields'
|
||||
> & {
|
||||
categories?: Array<{
|
||||
_id?: string
|
||||
id?: string
|
||||
slug?: string
|
||||
name?: string
|
||||
}>
|
||||
tags?: Array<{
|
||||
_id?: string
|
||||
id?: string
|
||||
slug?: string
|
||||
name?: string
|
||||
}>
|
||||
fields?: Record<string, unknown>
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/infra/biocad-less-rest-api/types/index.ts
|
||||
export type { TermRecordItemExtended } from './term'
|
||||
```
|
||||
|
||||
`declare module` нацеливается на `../generated/data-contracts` — именно там живут generated-интерфейсы. Он используется для добавления отсутствующих полей. `Extended`-тип используется, когда нужно переопределить неточные поля, не трогая generated-файлы.
|
||||
|
||||
## Публичный API
|
||||
|
||||
```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'
|
||||
```
|
||||
|
||||
Наружу импортируют только из `infra/pet-store-rest-api`, не из `generated/`.
|
||||
|
||||
Если у модуля есть расширенные типы, они тоже реэкспортируются через `index.ts`:
|
||||
|
||||
```ts
|
||||
// src/infra/biocad-less-rest-api/index.ts
|
||||
export type { TermRecordItemExtended } from './types'
|
||||
```
|
||||
|
||||
## Регенерация
|
||||
|
||||
При изменении OpenAPI-схемы:
|
||||
|
||||
```bash
|
||||
npm run codegen:pet-store-rest-api
|
||||
```
|
||||
|
||||
Что меняется:
|
||||
|
||||
- Папка `generated/` — перезаписывается генератором целиком.
|
||||
- `client.ts`, `rest-api.ts`, `hooks/`, `types/`, `index.ts` — не трогаются автоматически.
|
||||
|
||||
Если после регенерации поменялись имена операций, сигнатуры или типы, это исправляется в ручном коде модуля: `rest-api.ts`, GET-хуки, minimal clients и реэкспорты.
|
||||
|
||||
## Следующий шаг
|
||||
|
||||
После генерации настройте транспорт в `client.ts` по разделу [Кастомизация HTTP-клиента](./http-client.md), соберите полный API в `rest-api.ts`, затем проверьте [использование REST-клиента](../../SKILL.md#использование-rest-клиента) или добавьте [GET-хук REST-клиента](../../SKILL.md#get-хуки-rest-клиента) для Client Components.
|
||||
168
.opencode/skills/rest-client/reference/canons/http-client.md
Normal file
168
.opencode/skills/rest-client/reference/canons/http-client.md
Normal file
@@ -0,0 +1,168 @@
|
||||
---
|
||||
title: Кастомизация HTTP-клиента
|
||||
description: Настройка транспорта REST-клиента через опции и хуки HttpClient.
|
||||
keywords: [rest, http, транспорт, авторизация, jwt, refresh token, onRequest, onError]
|
||||
---
|
||||
|
||||
# Кастомизация HTTP-клиента
|
||||
|
||||
Настройка транспорта REST-клиента через опции и хуки `HttpClient`.
|
||||
|
||||
## Где живёт кастомизация
|
||||
|
||||
Вся настройка транспорта — `baseUrl`, заголовки, авторизация, retry — задаётся в `client.ts` REST-модуля при создании `HttpClient`.
|
||||
|
||||
Не размещайте авторизацию, обработку 401 и логирование в компонентах, GET-хуках или обёртках над операциями: у транспорта одна точка настройки.
|
||||
|
||||
В generated/SDK-сценарии `HttpClient` импортируется из generated-кода или SDK-пакета. В ручном сценарии без OpenAPI `HttpClient` импортируется из runtime-зависимости `@gromlab/api-codegen`.
|
||||
|
||||
```ts
|
||||
import { HttpClient } from '@gromlab/api-codegen'
|
||||
```
|
||||
|
||||
## Опции HttpClient
|
||||
|
||||
`HttpClient` принимает плоский конфиг: стандартные `fetch`-опции задаются вместе с хуками клиента.
|
||||
|
||||
| Опция | Назначение |
|
||||
| --- | --- |
|
||||
| `baseUrl` | Базовый URL API. |
|
||||
| `headers` | Заголовки по умолчанию для всех запросов. |
|
||||
| `credentials` | Политика отправки cookies: `omit`, `same-origin`, `include`. |
|
||||
| `timeout` | Таймаут запроса в миллисекундах, работает через `AbortSignal`. |
|
||||
| `customFetch` | Замена стандартного `fetch`: тесты, SSR, custom transport. |
|
||||
| `paramsSerializer` | Кастомная сериализация query params в URL. |
|
||||
| `responseParser` | Кастомный парсинг response body. |
|
||||
| `onRequest` | Request-хук перед вызовом `fetch`. |
|
||||
| `onResponse` | Response-хук после успешного HTTP-ответа. |
|
||||
| `onError` | Error-хук для HTTP-ошибок, network errors и ошибок парсинга. |
|
||||
|
||||
Полный список опций — в README `@gromlab/api-codegen`.
|
||||
|
||||
## Контракт хуков
|
||||
|
||||
- `onRequest(params, context)` вызывается перед `fetch` и возвращает изменённые `params`.
|
||||
- `onResponse(response, context)` вызывается после успешного ответа и возвращает `response`.
|
||||
- `onError(error, context)` вызывается для HTTP-ошибок, network errors и ошибок парсинга.
|
||||
- `context` содержит `url`, `request`, `retryCount` и `retry()` — повтор текущего запроса.
|
||||
- `onError` должен либо бросить ошибку, либо вернуть fallback-значение, либо вернуть результат `context.retry()`. Если вернуть `undefined`, ошибка будет считаться обработанной, а вызывающий код получит `undefined` вместо исключения.
|
||||
- Для защищённых endpoints generated operation передаёт `secure: true`, поэтому авторизацию можно добавлять только там, где она нужна.
|
||||
|
||||
## JWT-авторизация
|
||||
|
||||
Токен добавляется в `onRequest` только для защищённых запросов и не перезаписывает явно переданный `Authorization`.
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-rest-api/client.ts
|
||||
export const petStoreHttpClient = new HttpClient({
|
||||
baseUrl: 'https://example.com/api',
|
||||
onRequest: (params) => {
|
||||
const token = localStorage.getItem('access_token')
|
||||
|
||||
if (!params.secure || !token) {
|
||||
return params
|
||||
}
|
||||
|
||||
const headers = new Headers(params.headers)
|
||||
|
||||
if (!headers.has('Authorization')) {
|
||||
headers.set('Authorization', `Bearer ${token}`)
|
||||
}
|
||||
|
||||
return {
|
||||
...params,
|
||||
headers,
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Refresh token
|
||||
|
||||
Обновление токена и повтор запроса выполняются в `onError` через `context.retry()`. `context.retryCount` защищает от бесконечного цикла повторов.
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-rest-api/client.ts
|
||||
import { ApiError, HttpClient } from './generated'
|
||||
|
||||
export const petStoreHttpClient = new HttpClient({
|
||||
baseUrl: 'https://example.com/api',
|
||||
onError: async (error, context) => {
|
||||
if (error instanceof ApiError && error.status === 401 && context.retryCount === 0) {
|
||||
await refreshToken()
|
||||
return context.retry()
|
||||
}
|
||||
|
||||
throw error
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
`ApiError` экспортируется из `generated/` и содержит `status`, `statusText`, `response`, `data` и исходный `request`.
|
||||
|
||||
## Логирование
|
||||
|
||||
```ts
|
||||
const petStoreHttpClient = new HttpClient({
|
||||
baseUrl: 'https://example.com/api',
|
||||
onResponse: (response, context) => {
|
||||
console.log(context.request.method, context.url, response.status)
|
||||
return response
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Сериализация query params
|
||||
|
||||
```ts
|
||||
const petStoreHttpClient = new HttpClient({
|
||||
baseUrl: 'https://example.com/api',
|
||||
paramsSerializer: (query) => {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
Object.entries(query).forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
params.set(key, value.join(','))
|
||||
return
|
||||
}
|
||||
|
||||
if (value !== undefined) {
|
||||
params.set(key, String(value))
|
||||
}
|
||||
})
|
||||
|
||||
return params.toString()
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Параметры одного вызова
|
||||
|
||||
Разовые настройки запроса не относятся к `HttpClient`. Они передаются последним аргументом operation:
|
||||
|
||||
```ts
|
||||
import { getPetById } from './generated/operations/get-pet-by-id'
|
||||
import { petStoreHttpClient } from './client'
|
||||
|
||||
await getPetById(
|
||||
petStoreHttpClient,
|
||||
{ petId },
|
||||
{
|
||||
headers: {
|
||||
'X-Request-Id': requestId,
|
||||
},
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
## Правила
|
||||
|
||||
- Кастомизация транспорта живёт только в `client.ts` REST-модуля.
|
||||
- Авторизация добавляется в `onRequest` с учётом `params.secure` и без перезаписи явного `Authorization`.
|
||||
- `onError` либо бросает ошибку, либо возвращает fallback или `context.retry()`; молчаливый `return` запрещён.
|
||||
- Повторы запроса ограничиваются проверкой `context.retryCount`.
|
||||
- Бизнес-реакции на ошибки — тосты, редиректы, UI-состояние — не размещаются в хуках `HttpClient`.
|
||||
|
||||
## Следующий шаг
|
||||
|
||||
После настройки транспорта проверьте [использование REST-клиента](../../SKILL.md#использование-rest-клиента) или добавьте [GET-хуки REST-клиента](../../SKILL.md#get-хуки-rest-клиента).
|
||||
305
.opencode/skills/rest-client/reference/canons/manual.md
Normal file
305
.opencode/skills/rest-client/reference/canons/manual.md
Normal file
@@ -0,0 +1,305 @@
|
||||
---
|
||||
title: Ручное создание REST-клиента
|
||||
description: Создание generated-style REST-клиента вручную, когда OpenAPI нет или он неполный.
|
||||
keywords: [rest, ручной клиент, api-codegen, operation, ApiRequestClient, RequestParams, ContentType]
|
||||
---
|
||||
|
||||
# Ручное создание REST-клиента
|
||||
|
||||
Ручной клиент используется, когда у API нет OpenAPI-спецификации или она недостаточно точная для автогенерации.
|
||||
|
||||
Ручной режим не является отдельной архитектурой. Это тот же generated-style клиент, где operation-функции написаны вручную вместо генерации из OpenAPI.
|
||||
|
||||
## Зависимость
|
||||
|
||||
Для ручного клиента установите `@gromlab/api-codegen@5.1.0+` как runtime-зависимость проекта.
|
||||
|
||||
```bash
|
||||
bun add @gromlab/api-codegen
|
||||
```
|
||||
|
||||
Не используйте `npx @gromlab/api-codegen` для ручного режима: `npx` нужен для генерации SDK из OpenAPI, а ручной клиент импортирует runtime API пакета напрямую.
|
||||
|
||||
## Что нужно создать
|
||||
|
||||
```text
|
||||
src/infra/
|
||||
└── pet-project-rest-api/
|
||||
├── client.ts
|
||||
├── rest-api.ts
|
||||
├── operations-tree.ts
|
||||
├── operations/
|
||||
│ ├── posts-create.ts
|
||||
│ ├── posts-detail.ts
|
||||
│ ├── posts-list.ts
|
||||
│ └── index.ts
|
||||
├── hooks/
|
||||
│ ├── lib/
|
||||
│ │ └── create-query-string.ts
|
||||
│ ├── use-get-post-list.hook.ts
|
||||
│ └── index.ts
|
||||
├── types/
|
||||
│ ├── post.ts
|
||||
│ └── index.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
| Файл | Роль |
|
||||
|------|------|
|
||||
| `client.ts` | Настройка и экспорт `*HttpClient` |
|
||||
| `operations/` | Ручные operation-функции в стиле generated SDK |
|
||||
| `operations-tree.ts` | Ручное дерево операций для `createApiClient` |
|
||||
| `rest-api.ts` | Полный bound API через `createApiClient` |
|
||||
| `types/` | DTO запросов, ответов и именованные response-типы |
|
||||
| `hooks/` | GET-хуки REST-клиента, если данные нужны в Client Components |
|
||||
| `index.ts` | Публичный API REST-модуля |
|
||||
|
||||
## Типы API
|
||||
|
||||
DTO запросов и ответов живут в `types/`. `client.ts` и operation-файлы не объявляют доменные типы.
|
||||
|
||||
```ts
|
||||
// src/infra/pet-project-rest-api/types/post.ts
|
||||
export type PostDto = {
|
||||
id: string
|
||||
slug: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export type PostListQueryDto = {
|
||||
limit?: number
|
||||
category?: string
|
||||
}
|
||||
|
||||
export type CreatePostPayload = {
|
||||
title: string
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/infra/pet-project-rest-api/types/index.ts
|
||||
export type { CreatePostPayload, PostDto, PostListQueryDto } from './post'
|
||||
```
|
||||
|
||||
Если данным нужен доменный смысл или маппинг DTO, делайте это выше, в `business/`, а не в REST-клиенте.
|
||||
|
||||
## Operation-Функции
|
||||
|
||||
Каждая ручная operation повторяет контракт generated operation:
|
||||
|
||||
- первым аргументом принимает `http: ApiRequestClient`;
|
||||
- принимает typed params/body как последующие аргументы;
|
||||
- последним аргументом принимает `requestParams: RequestParams = {}`;
|
||||
- внутри вызывает только `http.request(...)`;
|
||||
- не использует прямой `fetch`;
|
||||
- не знает про React, SWR, UI и бизнес-логику.
|
||||
|
||||
```ts
|
||||
// src/infra/pet-project-rest-api/operations/posts-list.ts
|
||||
import type { ApiRequestClient, RequestParams } from '@gromlab/api-codegen'
|
||||
import type { PostDto, PostListQueryDto } from '../types'
|
||||
|
||||
export const postsList = (
|
||||
http: ApiRequestClient,
|
||||
query: PostListQueryDto = {},
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<PostDto[]>({
|
||||
path: '/posts',
|
||||
method: 'GET',
|
||||
query,
|
||||
format: 'json',
|
||||
...requestParams,
|
||||
})
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/infra/pet-project-rest-api/operations/posts-detail.ts
|
||||
import type { ApiRequestClient, RequestParams } from '@gromlab/api-codegen'
|
||||
import type { PostDto } from '../types'
|
||||
|
||||
export type PostsDetailParams = {
|
||||
slug: string
|
||||
}
|
||||
|
||||
export const postsDetail = (
|
||||
http: ApiRequestClient,
|
||||
{ slug }: PostsDetailParams,
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<PostDto>({
|
||||
path: `/posts/${slug}`,
|
||||
method: 'GET',
|
||||
format: 'json',
|
||||
...requestParams,
|
||||
})
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/infra/pet-project-rest-api/operations/posts-create.ts
|
||||
import { ContentType } from '@gromlab/api-codegen'
|
||||
import type { ApiRequestClient, RequestParams } from '@gromlab/api-codegen'
|
||||
import type { CreatePostPayload, PostDto } from '../types'
|
||||
|
||||
export const postsCreate = (
|
||||
http: ApiRequestClient,
|
||||
body: CreatePostPayload,
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<PostDto>({
|
||||
path: '/posts',
|
||||
method: 'POST',
|
||||
body,
|
||||
type: ContentType.Json,
|
||||
format: 'json',
|
||||
secure: true,
|
||||
...requestParams,
|
||||
})
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/infra/pet-project-rest-api/operations/index.ts
|
||||
export { postsCreate } from './posts-create'
|
||||
export { postsDetail } from './posts-detail'
|
||||
export { postsList } from './posts-list'
|
||||
export type { PostsDetailParams } from './posts-detail'
|
||||
```
|
||||
|
||||
## Транспорт
|
||||
|
||||
`client.ts` настраивает и экспортирует только `*HttpClient`.
|
||||
|
||||
```ts
|
||||
// src/infra/pet-project-rest-api/client.ts
|
||||
import { HttpClient } from '@gromlab/api-codegen'
|
||||
|
||||
export const petProjectHttpClient = new HttpClient({
|
||||
baseUrl: 'https://example.com/api',
|
||||
})
|
||||
```
|
||||
|
||||
Авторизация, refresh token, логирование и другие настройки транспорта задаются опциями и хуками `HttpClient` в этом же файле: [Кастомизация HTTP-клиента](./http-client.md).
|
||||
|
||||
## Operations Tree
|
||||
|
||||
`operations-tree.ts` вручную собирает дерево операций. Держите структуру такой, какой вы ожидаете видеть после будущей автогенерации.
|
||||
|
||||
```ts
|
||||
// src/infra/pet-project-rest-api/operations-tree.ts
|
||||
import { postsCreate } from './operations/posts-create'
|
||||
import { postsDetail } from './operations/posts-detail'
|
||||
import { postsList } from './operations/posts-list'
|
||||
|
||||
export const operationsTree = {
|
||||
posts: {
|
||||
create: postsCreate,
|
||||
detail: postsDetail,
|
||||
list: postsList,
|
||||
},
|
||||
} as const
|
||||
```
|
||||
|
||||
## Полный API-Клиент
|
||||
|
||||
`rest-api.ts` собирает полный bound API через тот же `createApiClient`, что используется в generated SDK.
|
||||
|
||||
```ts
|
||||
// src/infra/pet-project-rest-api/rest-api.ts
|
||||
import { createApiClient } from '@gromlab/api-codegen'
|
||||
import { petProjectHttpClient } from './client'
|
||||
import { operationsTree } from './operations-tree'
|
||||
|
||||
export const petProjectRestApi = createApiClient(
|
||||
petProjectHttpClient,
|
||||
operationsTree,
|
||||
)
|
||||
```
|
||||
|
||||
После binding внешний вызов совпадает с generated-клиентом:
|
||||
|
||||
```ts
|
||||
await petProjectRestApi.posts.create({ title: 'Новый пост' })
|
||||
await petProjectRestApi.posts.detail({ slug: 'hello' })
|
||||
```
|
||||
|
||||
## GET-Хуки
|
||||
|
||||
GET-хуки ручного клиента пишутся так же, как hooks для generated operations: импортируют точечную operation и вызывают её через общий `*HttpClient`.
|
||||
|
||||
```ts
|
||||
// src/infra/pet-project-rest-api/hooks/use-get-post-list.hook.ts
|
||||
import type { SWRConfiguration } from 'swr'
|
||||
import useSWR from 'swr'
|
||||
import { petProjectHttpClient } from '../client'
|
||||
import { postsList } from '../operations/posts-list'
|
||||
import type { PostDto, PostListQueryDto } from '../types'
|
||||
|
||||
export const getPostListKey = (params: PostListQueryDto = {}) => {
|
||||
const searchParams = new URLSearchParams()
|
||||
|
||||
if (params.limit !== undefined) {
|
||||
searchParams.set('limit', String(params.limit))
|
||||
}
|
||||
|
||||
if (params.category) {
|
||||
searchParams.set('category', params.category)
|
||||
}
|
||||
|
||||
const search = searchParams.toString()
|
||||
|
||||
return ['pet-project-rest-api', `/posts${search ? `?${search}` : ''}`] as const
|
||||
}
|
||||
|
||||
export const useGetPostList = (
|
||||
params: PostListQueryDto = {},
|
||||
config?: SWRConfiguration<PostDto[]>,
|
||||
) => {
|
||||
const key = getPostListKey(params)
|
||||
const fetcher = () => postsList(petProjectHttpClient, params)
|
||||
|
||||
return useSWR<PostDto[]>(key, fetcher, config)
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/infra/pet-project-rest-api/hooks/index.ts
|
||||
'use client'
|
||||
|
||||
export { getPostListKey, useGetPostList } from './use-get-post-list.hook'
|
||||
```
|
||||
|
||||
## Публичный API
|
||||
|
||||
```ts
|
||||
// src/infra/pet-project-rest-api/index.ts
|
||||
export { petProjectHttpClient } from './client'
|
||||
export { petProjectRestApi } from './rest-api'
|
||||
export * from './hooks'
|
||||
export type { PostsDetailParams } from './operations'
|
||||
export type { CreatePostPayload, PostDto, PostListQueryDto } from './types'
|
||||
```
|
||||
|
||||
Внешний код импортирует только из `infra/pet-project-rest-api`, не из внутренних файлов модуля.
|
||||
|
||||
## Миграция На OpenAPI
|
||||
|
||||
Ручной клиент проектируйте так, чтобы его можно было заменить автогенерацией без смены API потребителей.
|
||||
|
||||
- Называйте operation-функции и дерево близко к будущим `operationId` и tags OpenAPI.
|
||||
- Держите сигнатуры operation в стиле generated SDK: `operation(http, params/body, requestParams?)`.
|
||||
- Не создавайте собственный класс клиента и методные фабрики.
|
||||
- Если позже появится OpenAPI, сгенерируйте SDK и замените ручные operations на generated operations с минимальными правками `rest-api.ts`, GET-хуков и re-exports.
|
||||
|
||||
## Правила
|
||||
|
||||
- Ручной клиент использует `@gromlab/api-codegen@5.1.0+` как runtime dependency.
|
||||
- `client.ts` экспортирует только `*HttpClient`.
|
||||
- Ручные запросы живут в `operations/` и пишутся как generated-style operations.
|
||||
- `operations-tree.ts` вручную собирает дерево для `createApiClient`.
|
||||
- `rest-api.ts` экспортирует полный `*RestApi` через `createApiClient(*HttpClient, operationsTree)`.
|
||||
- GET-хуки вызывают точечные operations через `*HttpClient`, не полный `*RestApi`.
|
||||
- Не используйте прямой `fetch`, custom `RestApiClient` class, `methods/*Methods(client)` и ручные `get/post` wrappers.
|
||||
- DTO запросов и ответов живут в `types/`.
|
||||
- Доменные типы и маппинг DTO живут не в REST-клиенте, а в `business/`.
|
||||
|
||||
Следующий шаг: [Использование REST-клиента](../../SKILL.md#использование-rest-клиента), [GET-хуки REST-клиента](../../SKILL.md#get-хуки-rest-клиента) или выбор route-level data fetching по `nextjs-style-guide`.
|
||||
166
.opencode/skills/rest-client/reference/canons/sdk.md
Normal file
166
.opencode/skills/rest-client/reference/canons/sdk.md
Normal file
@@ -0,0 +1,166 @@
|
||||
---
|
||||
title: SDK-пакет REST-клиента
|
||||
description: Вынос generated REST-клиента в npm-пакет или пакет монорепозитория.
|
||||
keywords: [rest, sdk, npm, монорепозиторий, api-codegen, generated, пакет]
|
||||
---
|
||||
|
||||
# SDK-пакет REST-клиента
|
||||
|
||||
Вынос generated REST-клиента в npm-пакет или пакет монорепозитория.
|
||||
|
||||
## Когда выносить
|
||||
|
||||
SDK-пакет нужен, когда один и тот же API используется несколькими приложениями: в монорепозитории или через публикацию в npm registry.
|
||||
|
||||
Если API нужен одному приложению, SDK-пакет не создаётся — генерация идёт классически внутрь infra-модуля в `{name}-rest-api/generated` по разделу [Автогенерация из OpenAPI](./auto.md).
|
||||
|
||||
## Нейминг
|
||||
|
||||
SDK-пакет называется `{name}-rest-api-sdk`.
|
||||
|
||||
```text
|
||||
pet-store-rest-api-sdk
|
||||
@company/pet-store-rest-api-sdk
|
||||
```
|
||||
|
||||
Имя SDK-пакета образуется от имени infra-модуля: приложение с модулем `infra/pet-store-rest-api` потребляет пакет `pet-store-rest-api-sdk`.
|
||||
|
||||
## Что содержит SDK
|
||||
|
||||
SDK-пакет содержит только generated-код и `package.json` exports для точечных импортов. Это транспорт-нейтральная библиотека: она не знает про приложение, авторизацию и SWR конкретного проекта.
|
||||
|
||||
В SDK не размещаются:
|
||||
|
||||
- `client.ts` и `rest-api.ts` — настройка транспорта и bound API живут в приложении;
|
||||
- GET-хуки — SWR-обёртки живут в infra-модуле приложения;
|
||||
- бизнес-логика и DTO-маппинг.
|
||||
|
||||
## Структура пакета
|
||||
|
||||
```text
|
||||
packages/pet-store-rest-api-sdk/
|
||||
├── package.json
|
||||
└── src/
|
||||
└── generated/
|
||||
```
|
||||
|
||||
Скрипт генерации внутри пакета выводит split-клиент в `src/generated`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"codegen": "npx @gromlab/api-codegen@latest -i https://petstore3.swagger.io/api/v3/openapi.json -o src/generated"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`package.json` обязан открыть subpath exports для generated частей:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@company/pet-store-rest-api-sdk",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/generated/index.ts",
|
||||
"./create-api-client": "./src/generated/create-api-client.ts",
|
||||
"./data-contracts": "./src/generated/data-contracts.ts",
|
||||
"./http-client": "./src/generated/http-client.ts",
|
||||
"./operations": "./src/generated/operations/index.ts",
|
||||
"./operations/*": "./src/generated/operations/*.ts",
|
||||
"./operations-tree": "./src/generated/operations-tree.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Файлы в `src/generated/` не правятся руками и коммитятся в репозиторий пакета.
|
||||
|
||||
Корневой `package.json` монорепозитория добавляет удобный script для запуска codegen через workspace filter:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"codegen:pet-store-rest-api-sdk": "dotenv -- pnpm --filter @company/pet-store-rest-api-sdk run codegen"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Потребление в приложении
|
||||
|
||||
Приложение оформляет REST-модуль как обычно: `src/infra/{name}-rest-api/` с `client.ts`, `rest-api.ts`, `hooks/`, `types/` и корневым `index.ts`. Меняется только источник generated-кода: вместо локальной папки `generated/` импортируется SDK-пакет.
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-rest-api/client.ts
|
||||
import { HttpClient } from '@company/pet-store-rest-api-sdk'
|
||||
|
||||
export const petStoreHttpClient = new HttpClient({
|
||||
baseUrl: 'https://example.com/api',
|
||||
})
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-rest-api/rest-api.ts
|
||||
import { createApiClient } from '@company/pet-store-rest-api-sdk/create-api-client'
|
||||
import { operationsTree } from '@company/pet-store-rest-api-sdk/operations-tree'
|
||||
import { petStoreHttpClient } from './client'
|
||||
|
||||
export const petStoreRestApi = createApiClient(petStoreHttpClient, operationsTree)
|
||||
```
|
||||
|
||||
Типы в хуках и `types/` импортируются из пакета вместо `../generated`:
|
||||
|
||||
```ts
|
||||
import type { GetPetByIdParams, Pet } from '@company/pet-store-rest-api-sdk'
|
||||
```
|
||||
|
||||
GET-хуки импортируют точечные operations из SDK subpath:
|
||||
|
||||
```ts
|
||||
import { getPetById } from '@company/pet-store-rest-api-sdk/operations/get-pet-by-id'
|
||||
```
|
||||
|
||||
Остальной контракт модуля не меняется:
|
||||
|
||||
- настройка транспорта — [Кастомизация HTTP-клиента](./http-client.md);
|
||||
- GET-хуки — [GET-хуки REST-клиента](../../SKILL.md#get-хуки-rest-клиента);
|
||||
- внешний код импортирует только из `infra/pet-store-rest-api`, не из SDK-пакета напрямую.
|
||||
|
||||
## Minimal Client В Composition
|
||||
|
||||
SDK operations можно импортировать напрямую в boundary-файлах feature/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,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Это исключение действует только для boundary-файлов сборки клиента. UI/components, pages и произвольные helpers не импортируют SDK напрямую.
|
||||
|
||||
## Регенерация
|
||||
|
||||
При изменении OpenAPI-схемы перегенерируется `src/generated` внутри SDK-пакета:
|
||||
|
||||
```bash
|
||||
npm run codegen
|
||||
```
|
||||
|
||||
Приложения получают обновление через новую версию пакета. Если поменялись имена операций или типы, правки в приложении локализованы в `rest-api.ts`, GET-хуках, minimal clients и реэкспортах.
|
||||
|
||||
## Правила
|
||||
|
||||
- SDK-пакет называется `{name}-rest-api-sdk` и содержит только generated-код.
|
||||
- SDK-пакет обязан открыть subpath exports: `.`, `./operations/*`, `./create-api-client`, `./operations-tree`, `./http-client`, `./data-contracts`.
|
||||
- Генерация внутри пакета идёт в `src/generated`, файлы не правятся руками.
|
||||
- `client.ts`, `rest-api.ts`, авторизация и GET-хуки живут в infra-модуле приложения, не в SDK.
|
||||
- Приложение импортирует SDK внутри своего `infra/{name}-rest-api` модуля.
|
||||
- Boundary-файл feature/composition может импортировать SDK operations напрямую только для сборки minimal client.
|
||||
- UI/components не импортируют SDK напрямую.
|
||||
135
.opencode/skills/rest-client/reference/canons/setup.md
Normal file
135
.opencode/skills/rest-client/reference/canons/setup.md
Normal file
@@ -0,0 +1,135 @@
|
||||
---
|
||||
title: Настройка REST-клиента
|
||||
description: Подготовка REST-клиента сервиса к использованию.
|
||||
keywords: [rest, клиент, infra, operation, openapi, get-хуки, swr]
|
||||
---
|
||||
|
||||
# Настройка REST-клиента
|
||||
|
||||
Подготовка REST-клиента сервиса к использованию.
|
||||
|
||||
## Что настраиваем
|
||||
|
||||
REST-клиент — это infra-модуль, через который проект работает с внешним REST API.
|
||||
|
||||
На этапе настройки нужно подготовить транспортный HTTP-клиент, полный API-клиент и GET-хуки для клиентских компонентов.
|
||||
|
||||
## Нейминг
|
||||
|
||||
- infra-модуль REST-клиента называется `{name}-rest-api`: `src/infra/pet-store-rest-api/`.
|
||||
- Генерация внутри клиента — классический вариант — живёт в `{name}-rest-api/generated`.
|
||||
- Если generated-клиент выносится в npm-пакет или пакет монорепозитория, пакет называется `{name}-rest-api-sdk`: [SDK-пакет REST-клиента](./sdk.md).
|
||||
- Производные имена образуются от имени модуля: транспорт `petStoreHttpClient`, полный API-клиент `petStoreRestApi`, SWR-ключ `['pet-store-rest-api', ...]`.
|
||||
|
||||
## Из чего состоит клиент
|
||||
|
||||
REST-клиент состоит из четырёх основных частей:
|
||||
|
||||
1. **Транспорт** — ручной `client.ts` с настроенным `*HttpClient`.
|
||||
2. **Полный API-клиент** — `rest-api.ts` с `createApiClient(*HttpClient, operationsTree)`.
|
||||
3. **Операции** — operation-функции, сгенерированные из OpenAPI, поставляемые SDK-пакетом или написанные вручную через `@gromlab/api-codegen`.
|
||||
4. **GET-хуки** — SWR-обёртки для GET-запросов.
|
||||
|
||||
Эти части живут в одном REST-модуле, потому что относятся к одному внешнему сервису.
|
||||
|
||||
## Транспорт
|
||||
|
||||
`client.ts` — ручной слой, который настраивает транспорт: `HttpClient`, заголовки, авторизацию и обработку ошибок.
|
||||
|
||||
Авторизация, refresh token и другие транспортные сценарии настраиваются хуками `HttpClient` — `onRequest`, `onResponse`, `onError`: [Кастомизация HTTP-клиента](./http-client.md).
|
||||
|
||||
Даже если операции генерируются из OpenAPI, `client.ts` остаётся ручным файлом проекта.
|
||||
|
||||
`client.ts` экспортирует только настроенный `*HttpClient`. В нём не размещаются DTO, `declare module`, `Extended`-типы, GET-хуки, `operationsTree`, `createApiClient` и бизнес-логика.
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-rest-api/client.ts
|
||||
import { HttpClient } from '@company/pet-store-rest-api-sdk'
|
||||
|
||||
export const petStoreHttpClient = new HttpClient({
|
||||
baseUrl: 'https://example.com/api',
|
||||
})
|
||||
```
|
||||
|
||||
## Полный API-клиент
|
||||
|
||||
`rest-api.ts` собирает полный bound API-клиент из всего generated дерева операций.
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-rest-api/rest-api.ts
|
||||
import { createApiClient } from '@company/pet-store-rest-api-sdk/create-api-client'
|
||||
import { operationsTree } from '@company/pet-store-rest-api-sdk/operations-tree'
|
||||
import { petStoreHttpClient } from './client'
|
||||
|
||||
export const petStoreRestApi = createApiClient(petStoreHttpClient, operationsTree)
|
||||
```
|
||||
|
||||
Импортируйте `operationsTree` только в `rest-api.ts`. Для GET-хуков и minimal clients используйте точечные operation imports.
|
||||
|
||||
## Операции
|
||||
|
||||
Операции описывают конкретные запросы к API.
|
||||
|
||||
Они появляются одним из трёх способов:
|
||||
|
||||
- генерируются из OpenAPI в `generated/` как отдельные operation-функции;
|
||||
- поставляются SDK-пакетом `{name}-rest-api-sdk`;
|
||||
- создаются вручную в `operations/` через `@gromlab/api-codegen`, если OpenAPI нет или он неполный.
|
||||
|
||||
Подробности:
|
||||
|
||||
- [Автогенерация из OpenAPI](./auto.md)
|
||||
- [Ручное создание](./manual.md)
|
||||
|
||||
## GET-хуки
|
||||
|
||||
Для GET-запросов добавляются GET-хуки REST-клиента.
|
||||
|
||||
Это прозрачные SWR-обёртки над generated GET operations. Они живут в `hooks/` этого же REST-модуля и нужны для использования данных в Client Components.
|
||||
|
||||
GET-хуки именуются с префиксом `useGet`: `useGetPetList`, `useGetPetDetail`, `useGetCurrentUser`.
|
||||
|
||||
Каждый GET-хук имеет экспортируемую key-функцию. SWR-ключ всегда имеет формат `[serviceName, endpoint]`: например `['pet-store-rest-api', '/pet/10']`.
|
||||
|
||||
Хук принимает generated-параметры операции и SWR-настройки: `params?: GetPetByIdParams | null`, `config?: SWRConfiguration<Pet>`.
|
||||
|
||||
`hooks/index.ts` содержит `'use client'` и экспортирует все GET-хуки. Сами `use-get-*.hook.ts` остаются обычными файлами без директивы.
|
||||
|
||||
Подробности:
|
||||
|
||||
- [GET-хуки REST-клиента](../../SKILL.md#get-хуки-rest-клиента)
|
||||
|
||||
## Структура модуля
|
||||
|
||||
```text
|
||||
src/infra/{name}-rest-api/
|
||||
├── client.ts # настройка и экспорт *HttpClient
|
||||
├── rest-api.ts # полный *RestApi через operationsTree
|
||||
├── generated/ или operations/ # локальный split-клиент или ручные operations
|
||||
├── operations-tree.ts # ручное дерево операций, если нет generated/operations-tree.ts
|
||||
├── hooks/ # GET-хуки REST-клиента
|
||||
│ ├── lib/
|
||||
│ │ └── create-query-string.ts
|
||||
│ ├── use-get-*.hook.ts
|
||||
│ └── index.ts # 'use client' и публичные экспорты hooks
|
||||
├── types/ # DTO, именованные response-типы и расширения типов
|
||||
├── errors/ # ошибки API, если нужны
|
||||
└── index.ts # публичный API
|
||||
```
|
||||
|
||||
`index.ts` — единственная точка входа в REST-модуль для внешнего кода.
|
||||
|
||||
Если generated-код вынесен в `{name}-rest-api-sdk`, локальной папки `generated/` внутри infra-модуля может не быть: `client.ts`, `rest-api.ts` и GET-хуки импортируют generated части из SDK subpath exports.
|
||||
|
||||
Если OpenAPI нет, не создавайте самописный `fetch`-класс и `methods/`. Ручной клиент пишется тем же API, что generated-клиент: `HttpClient`, `ApiRequestClient`, `RequestParams`, operation-функции и `createApiClient` из `@gromlab/api-codegen`.
|
||||
|
||||
Если generated operation возвращает безымянный тип вроде `Record<string, number>`, а этот тип нужен снаружи, вынесите его в `types/`. Не объявляйте DTO внутри `hooks/use-get-*.hook.ts`.
|
||||
|
||||
## Что делаем дальше
|
||||
|
||||
1. Создайте операции клиента: [Автогенерация из OpenAPI](./auto.md), SDK-пакет или [Ручное создание](./manual.md).
|
||||
2. Если клиент нужен нескольким приложениям, вынесите generated-код в пакет: [SDK-пакет REST-клиента](./sdk.md).
|
||||
3. Настройте транспорт — авторизацию, хуки, таймауты: [Кастомизация HTTP-клиента](./http-client.md).
|
||||
4. Добавьте GET-хуки для GET-запросов: [GET-хуки REST-клиента](../../SKILL.md#get-хуки-rest-клиента).
|
||||
5. Проверьте прямые вызовы клиента: [Использование REST-клиента](../../SKILL.md#использование-rest-клиента).
|
||||
6. После настройки клиента выбирайте стратегию route-level data fetching по `nextjs-style-guide`.
|
||||
Reference in New Issue
Block a user