forked from templates/nextjs-template
style: Обновление код стайла
This commit is contained in:
272
ai/nextjs-style-guide/applied/rest-client/setup/auto.md
Normal file
272
ai/nextjs-style-guide/applied/rest-client/setup/auto.md
Normal file
@@ -0,0 +1,272 @@
|
||||
---
|
||||
title: Автогенерация REST-клиента
|
||||
description: Генерация REST-клиента из OpenAPI-спецификации.
|
||||
keywords: [rest, openapi, api-codegen, автогенерация, generated, npx]
|
||||
---
|
||||
|
||||
# Автогенерация REST-клиента
|
||||
|
||||
Генерация REST-клиента из OpenAPI-спецификации.
|
||||
|
||||
## Когда использовать
|
||||
|
||||
Автогенерация используется, когда у API есть актуальная OpenAPI-спецификация. Генератор создаёт TypeScript-клиент, типы и методы API, а разработчик вручную добавляет настройку клиента и GET-хуки.
|
||||
|
||||
## Пример API
|
||||
|
||||
В примерах используется Swagger Petstore:
|
||||
|
||||
```text
|
||||
https://petstore3.swagger.io/api/v3/openapi.json
|
||||
```
|
||||
|
||||
Имена модуля:
|
||||
|
||||
```text
|
||||
src/infra/pet-store-api/
|
||||
petStoreApi
|
||||
pet-store-api.generated.ts
|
||||
```
|
||||
|
||||
## Скрипт генерации
|
||||
|
||||
`@gromlab/api-codegen` не устанавливается в `devDependencies`. Используем `npx @gromlab/api-codegen@latest`, чтобы запускать свежую версию.
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"codegen:pet-store-api": "npx @gromlab/api-codegen@latest -i https://petstore3.swagger.io/api/v3/openapi.json -o src/infra/pet-store-api/generated -n pet-store-api.generated"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Параметры:
|
||||
|
||||
- `-i` — путь к OpenAPI-спецификации: URL или локальный файл.
|
||||
- `-o` — директория для сгенерированного файла.
|
||||
- `-n` — имя сгенерированного файла без `.ts`.
|
||||
|
||||
Ключ `--swr` не используется. GET-хуки REST-клиента пишутся вручную, чтобы сохранить проектный контракт: один GET-хук = один GET-метод, без бизнес-логики и композиции.
|
||||
|
||||
## Генерация
|
||||
|
||||
```bash
|
||||
npm run codegen:pet-store-api
|
||||
```
|
||||
|
||||
Ожидаемый результат:
|
||||
|
||||
```text
|
||||
src/infra/pet-store-api/generated/
|
||||
└── pet-store-api.generated.ts
|
||||
```
|
||||
|
||||
Сгенерированный файл не правится руками и коммитится в репозиторий.
|
||||
|
||||
## Проверка методов
|
||||
|
||||
После генерации откройте `generated/pet-store-api.generated.ts` и проверьте фактические имена методов.
|
||||
|
||||
Для Petstore нужны GET-операции вида:
|
||||
|
||||
```ts
|
||||
petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Available })
|
||||
petStoreApi.pet.getPetById({ petId: 10 })
|
||||
```
|
||||
|
||||
Точные сигнатуры зависят от OpenAPI-схемы и версии генератора. В рабочих задачах всегда сверяйтесь с generated-файлом.
|
||||
|
||||
## Алгоритм для агента
|
||||
|
||||
После генерации агент должен действовать по шагам:
|
||||
|
||||
1. Открыть `generated/{service-name}.generated.ts`.
|
||||
2. Найти фактические имена GET-методов клиента.
|
||||
3. Для каждого нужного GET-метода найти generated-тип параметров и тип ответа.
|
||||
4. Создать или обновить `client.ts` только для настройки транспорта и экспорта инстанса клиента.
|
||||
5. Создать GET-хуки только для реально нужных GET-методов, не для всех методов API на всякий случай.
|
||||
6. Для каждого GET-хука создать key-функцию формата `[serviceName, endpoint]`.
|
||||
7. В key-функции вернуть `null`, если обязательные параметры не готовы.
|
||||
8. В хуке принять `params?: GeneratedParams | null` и `config?: SWRConfiguration<Data>`.
|
||||
9. В fetcher вызвать generated-метод клиента с `params as GeneratedParams`.
|
||||
10. Экспортировать хук и key-функцию из `hooks/index.ts`.
|
||||
11. Экспортировать наружу только нужные generated-типы, generated enum, DTO и `hooks` через корневой `index.ts`.
|
||||
|
||||
Что агент не должен делать:
|
||||
|
||||
- Не использовать ключ `--swr` генератора.
|
||||
- Не править `generated/*.generated.ts` руками.
|
||||
- Не добавлять GET-хуки для POST, PUT, PATCH, DELETE.
|
||||
- Не добавлять бизнес-флаги, тосты, редиректы и UI-состояние в GET-хук.
|
||||
- Не создавать словари enum-маппинга внутри GET-хука.
|
||||
- Не объявлять DTO и response-типы в файле хука.
|
||||
- Не вызывать `useSWR` условно.
|
||||
- Не добавлять `throw` в fetcher для неготовых params.
|
||||
|
||||
## `client.ts`
|
||||
|
||||
Сгенерированный код не должен напрямую использоваться из приложения. Сначала создаётся настроенный инстанс клиента.
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-api/client.ts
|
||||
import { Api, HttpClient } from './generated/pet-store-api.generated'
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_PET_STORE_API_BASE_URL
|
||||
|
||||
if (!baseUrl) {
|
||||
throw new Error('NEXT_PUBLIC_PET_STORE_API_BASE_URL is required')
|
||||
}
|
||||
|
||||
const httpClient = new HttpClient({
|
||||
baseUrl,
|
||||
baseApiParams: {
|
||||
secure: false,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const petStoreApi = new Api(httpClient)
|
||||
```
|
||||
|
||||
Локальное значение `NEXT_PUBLIC_PET_STORE_API_BASE_URL` задаётся в `.env.local`. Не добавляйте fallback вроде `?? 'http://localhost:8080/api/v3'` или `?? ''`: если env-переменная не задана, клиент должен падать с явной ошибкой конфигурации.
|
||||
|
||||
`client.ts` не содержит расширения типов, `declare module` и `Extended`-типы. Он только настраивает транспорт и экспортирует инстанс клиента.
|
||||
|
||||
## GET-хуки
|
||||
|
||||
GET-хуки пишутся вручную после проверки generated-методов.
|
||||
|
||||
Пример для generated-метода `petStoreApi.pet.getPetById({ petId })`:
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-api/hooks/use-get-pet-detail.hook.ts
|
||||
import type { SWRConfiguration } from 'swr'
|
||||
import useSWR from 'swr'
|
||||
import { petStoreApi } from '../client'
|
||||
import type { GetPetByIdParams, Pet } from '../generated/pet-store-api.generated'
|
||||
|
||||
export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
|
||||
if (!params?.petId) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ['pet-store-api', `/pet/${params.petId}`] as const
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает детальную карточку питомца с кешированием результата.
|
||||
*/
|
||||
export const useGetPetDetail = (
|
||||
params?: GetPetByIdParams | null,
|
||||
config?: SWRConfiguration<Pet>,
|
||||
) => {
|
||||
const key = getPetDetailKey(params)
|
||||
const fetcher = () => petStoreApi.pet.getPetById(params as GetPetByIdParams)
|
||||
|
||||
return useSWR<Pet>(key, fetcher, config)
|
||||
}
|
||||
```
|
||||
|
||||
Подробный контракт key-функций, `params`, `config` и запретов описан в разделе [GET-хуки REST-клиента](./hooks.md).
|
||||
|
||||
## Расширение сгенерированных типов
|
||||
|
||||
Сгенерированный файл не правится руками. Если OpenAPI-спецификация неполная или генератор дал слишком общий тип (`object`, `unknown`, отсутствующее поле), расширения живут в `types/`.
|
||||
|
||||
```text
|
||||
src/infra/biocad-less-api/
|
||||
├── generated/
|
||||
│ └── biocad-less-api.generated.ts
|
||||
├── types/
|
||||
│ ├── term.ts
|
||||
│ └── index.ts
|
||||
├── client.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Пример расширения generated-типа:
|
||||
|
||||
```ts
|
||||
// src/infra/biocad-less-api/types/term.ts
|
||||
import type { TermRecordItem } from '../generated/biocad-less-api.generated'
|
||||
|
||||
declare module '../generated/biocad-less-api.generated' {
|
||||
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-api/types/index.ts
|
||||
export type { TermRecordItemExtended } from './term'
|
||||
```
|
||||
|
||||
`declare module` используется для добавления отсутствующих полей в generated-интерфейс. `Extended`-тип используется, когда нужно переопределить неточные поля, не трогая generated-файл.
|
||||
|
||||
## Публичный API
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-api/index.ts
|
||||
export { petStoreApi } from './client'
|
||||
export type {
|
||||
FindPetsByStatusParams,
|
||||
GetPetByIdParams,
|
||||
Pet,
|
||||
} from './generated/pet-store-api.generated'
|
||||
export { PetStatusEnum, StatusEnum } from './generated/pet-store-api.generated'
|
||||
export * from './hooks'
|
||||
```
|
||||
|
||||
Наружу импортируют только из `infra/pet-store-api`, не из `generated/`.
|
||||
|
||||
Если у модуля есть расширенные типы, они тоже реэкспортируются через `index.ts`:
|
||||
|
||||
```ts
|
||||
// src/infra/biocad-less-api/index.ts
|
||||
export type { TermRecordItemExtended } from './types'
|
||||
```
|
||||
|
||||
## Регенерация
|
||||
|
||||
При изменении OpenAPI-схемы:
|
||||
|
||||
```bash
|
||||
npm run codegen:pet-store-api
|
||||
```
|
||||
|
||||
Что меняется:
|
||||
|
||||
- `generated/pet-store-api.generated.ts` — перезаписывается генератором.
|
||||
- `client.ts`, `hooks/`, `types/`, `index.ts` — не трогаются автоматически.
|
||||
|
||||
Если после регенерации поменялись сигнатуры методов или типы, это исправляется в ручном коде модуля.
|
||||
|
||||
## Следующий шаг
|
||||
|
||||
После генерации и настройки `client.ts` проверьте [использование REST-клиента](../usage.md) или добавьте [GET-хук REST-клиента](./hooks.md) для Client Components.
|
||||
313
ai/nextjs-style-guide/applied/rest-client/setup/hooks.md
Normal file
313
ai/nextjs-style-guide/applied/rest-client/setup/hooks.md
Normal file
@@ -0,0 +1,313 @@
|
||||
---
|
||||
title: GET-хуки REST-клиента
|
||||
description: Прозрачные SWR-обёртки над GET-методами REST-клиента.
|
||||
keywords: [rest, swr, get-хуки, client components, infra]
|
||||
---
|
||||
|
||||
# GET-хуки REST-клиента
|
||||
|
||||
Прозрачные SWR-обёртки над GET-методами REST-клиента.
|
||||
|
||||
## Зачем нужны
|
||||
|
||||
GET-хуки нужны, чтобы Client Components получали REST-данные через SWR, но не работали с `useSWR`, ключами кеша и fetcher напрямую.
|
||||
|
||||
## Где лежат
|
||||
|
||||
GET-хуки принадлежат REST-клиенту конкретного сервиса и живут рядом с ним:
|
||||
|
||||
```text
|
||||
src/infra/
|
||||
└── pet-store-api/
|
||||
├── client.ts
|
||||
├── generated/
|
||||
├── hooks/
|
||||
│ ├── use-get-pet-list.hook.ts
|
||||
│ ├── use-get-pet-detail.hook.ts
|
||||
│ └── index.ts
|
||||
├── types/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## Контракт
|
||||
|
||||
- Один GET-хук = один GET-метод клиента.
|
||||
- Имя GET-хука начинается с `useGet`: `useGetPetList`, `useGetPetDetail`.
|
||||
- Имя файла начинается с `use-get`: `use-get-pet-list.hook.ts`.
|
||||
- Хук принимает `params?: GeneratedParams | null` и `config?: SWRConfiguration<Data>`.
|
||||
- Для GET-метода без параметров хук принимает только `config?: SWRConfiguration<Data>`.
|
||||
- Key-функция принимает те же `params`, что и хук.
|
||||
- Key-функция возвращает `null`, если обязательные параметры не готовы.
|
||||
- Проверка готовности запроса живёт в key-функции, а не в теле хука.
|
||||
- Хук вызывает `useSWR` один раз и безусловно.
|
||||
- Fetcher не проверяет `null`, не бросает ошибку и не вызывает метод клиента с `null`.
|
||||
- Внутри только SWR-механика: key, fetcher, `useSWR`, `config`.
|
||||
- Хук возвращает тип ответа API: generated-тип или DTO из `types/`.
|
||||
- Хук не объединяет несколько запросов.
|
||||
- Хук не маппит DTO в доменную модель.
|
||||
- Хук не вычисляет бизнес-флаги: `isAuth`, `canEdit`, `hasAccess`, `hasPets`.
|
||||
- Хук не вызывает тосты, модалки, редиректы и не пишет UI-состояние.
|
||||
|
||||
## Формат SWR-ключа
|
||||
|
||||
SWR-ключ GET-хука всегда создаётся отдельной экспортируемой функцией.
|
||||
|
||||
Формат ключа:
|
||||
|
||||
```ts
|
||||
['pet-store-api', '/pet/10'] as const
|
||||
```
|
||||
|
||||
- Первый элемент — имя API-сервиса или REST-клиента в `kebab-case`.
|
||||
- Второй элемент — endpoint запроса: path и query string.
|
||||
- Key-функция возвращает `null`, когда запрос нельзя выполнять.
|
||||
- Key-функция нужна и GET-хуку, и `SWRConfig fallback`.
|
||||
- Не используйте произвольные части вроде `['pet-store-api', 'pet', 'detail', params]`.
|
||||
- Не используйте только строку endpoint без имени сервиса.
|
||||
|
||||
Примеры ключей:
|
||||
|
||||
```ts
|
||||
export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
|
||||
if (!params?.petId) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ['pet-store-api', `/pet/${params.petId}`] as const
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
export const getPetListKey = (params?: FindPetsByStatusParams | null) => {
|
||||
if (!params?.status) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ['pet-store-api', `/pet/findByStatus?status=${params.status}`] as const
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
export const getPetListByTagsKey = (params?: FindPetsByTagsParams | null) => {
|
||||
if (!params?.tags.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ['pet-store-api', `/pet/findByTags?tags=${params.tags.join(',')}`] as const
|
||||
}
|
||||
```
|
||||
|
||||
Если API допускает `0` как валидный идентификатор, не используйте проверку `!params?.id`. В таком случае проверяйте `null` и `undefined` явно.
|
||||
|
||||
## Пример списка
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-api/hooks/use-get-pet-list.hook.ts
|
||||
import type { SWRConfiguration } from 'swr'
|
||||
import useSWR from 'swr'
|
||||
import { petStoreApi } from '../client'
|
||||
import type {
|
||||
FindPetsByStatusParams,
|
||||
Pet,
|
||||
} from '../generated/pet-store-api.generated'
|
||||
|
||||
export const getPetListKey = (params?: FindPetsByStatusParams | null) => {
|
||||
if (!params?.status) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ['pet-store-api', `/pet/findByStatus?status=${params.status}`] as const
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает список питомцев по статусу.
|
||||
*/
|
||||
export const useGetPetList = (
|
||||
params?: FindPetsByStatusParams | null,
|
||||
config?: SWRConfiguration<Pet[]>,
|
||||
) => {
|
||||
const key = getPetListKey(params)
|
||||
const fetcher = () => petStoreApi.pet.findPetsByStatus(
|
||||
params as FindPetsByStatusParams,
|
||||
)
|
||||
|
||||
return useSWR<Pet[]>(key, fetcher, config)
|
||||
}
|
||||
```
|
||||
|
||||
`params as FindPetsByStatusParams` допустим только в fetcher: готовность параметров проверена в key-функции, а при `key = null` SWR не вызывает fetcher.
|
||||
|
||||
## Пример detail-запроса
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-api/hooks/use-get-pet-detail.hook.ts
|
||||
import type { SWRConfiguration } from 'swr'
|
||||
import useSWR from 'swr'
|
||||
import { petStoreApi } from '../client'
|
||||
import type { GetPetByIdParams, Pet } from '../generated/pet-store-api.generated'
|
||||
|
||||
export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
|
||||
if (!params?.petId) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ['pet-store-api', `/pet/${params.petId}`] as const
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает детальную карточку питомца с кешированием результата.
|
||||
*/
|
||||
export const useGetPetDetail = (
|
||||
params?: GetPetByIdParams | null,
|
||||
config?: SWRConfiguration<Pet>,
|
||||
) => {
|
||||
const key = getPetDetailKey(params)
|
||||
const fetcher = () => petStoreApi.pet.getPetById(params as GetPetByIdParams)
|
||||
|
||||
return useSWR<Pet>(key, fetcher, config)
|
||||
}
|
||||
```
|
||||
|
||||
## Пример без параметров
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-api/hooks/use-get-store-inventory.hook.ts
|
||||
import type { SWRConfiguration } from 'swr'
|
||||
import useSWR from 'swr'
|
||||
import { petStoreApi } from '../client'
|
||||
import type { StoreInventory } from '../types'
|
||||
|
||||
export const getStoreInventoryKey = () => {
|
||||
return ['pet-store-api', '/store/inventory'] as const
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает инвентарь магазина.
|
||||
*/
|
||||
export const useGetStoreInventory = (
|
||||
config?: SWRConfiguration<StoreInventory>,
|
||||
) => {
|
||||
return useSWR<StoreInventory>(
|
||||
getStoreInventoryKey(),
|
||||
() => petStoreApi.store.getInventory(),
|
||||
config,
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Если generated-метод возвращает безымянный тип вроде `Record<string, number>`, а тип нужен наружу, вынесите его в `types/`.
|
||||
|
||||
## Отложенный запрос
|
||||
|
||||
GET-хук может принимать `null` или `undefined` для обязательных параметров. Это означает, что параметры ещё не готовы и запрос выполнять нельзя.
|
||||
|
||||
```ts
|
||||
const key = getPetDetailKey(params)
|
||||
```
|
||||
|
||||
Если `params` не готов, key-функция вернёт `null`. SWR не вызовет fetcher для `null`-ключа.
|
||||
|
||||
Не добавляйте отдельные `isReady`, `throw new Error(...)` и условный вызов `useSWR`.
|
||||
|
||||
## Экспорт
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-api/hooks/index.ts
|
||||
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-api/index.ts
|
||||
export { petStoreApi } from './client'
|
||||
export type {
|
||||
FindPetsByStatusParams,
|
||||
GetPetByIdParams,
|
||||
Pet,
|
||||
} from './generated/pet-store-api.generated'
|
||||
export { PetStatusEnum, StatusEnum } from './generated/pet-store-api.generated'
|
||||
export * from './hooks'
|
||||
export type { StoreInventory } from './types'
|
||||
```
|
||||
|
||||
Наружу импортируют только из `infra/pet-store-api`, не из `generated/` и не из `hooks/` напрямую.
|
||||
|
||||
## Где заканчивается infra
|
||||
|
||||
```ts
|
||||
// Хорошо: infra, прозрачный GET-хук
|
||||
const { data: pets } = useGetPetList({ status: StatusEnum.Available })
|
||||
```
|
||||
|
||||
```ts
|
||||
// Хорошо: business, доменная интерпретация
|
||||
export const useAvailablePets = () => {
|
||||
const query = useGetPetList({ status: StatusEnum.Available })
|
||||
|
||||
return {
|
||||
...query,
|
||||
hasPets: Boolean(query.data?.length),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`hasPets` — не часть GET-запроса, поэтому он не добавляется в `useGetPetList`.
|
||||
|
||||
## Что запрещено
|
||||
|
||||
```ts
|
||||
// Плохо — useSWR в компоненте
|
||||
const { data } = useSWR(
|
||||
['pet-store-api', '/pet/findByStatus?status=available'],
|
||||
() => petStoreApi.pet.findPetsByStatus({ status: StatusEnum.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 petStoreApi.pet.getPetById(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, () => petStoreApi.pet.getPetById(params))
|
||||
}
|
||||
|
||||
// Плохо — несколько GET внутри infra-хука
|
||||
export const usePetDashboard = () => {
|
||||
const available = useGetPetList({ status: StatusEnum.Available })
|
||||
const sold = useGetPetList({ status: StatusEnum.Sold })
|
||||
|
||||
return { available, sold }
|
||||
}
|
||||
|
||||
// Плохо — бизнес-флаг внутри GET-хука REST-клиента
|
||||
export const useGetPetList = (params?: FindPetsByStatusParams | null) => {
|
||||
const query = useSWR(...)
|
||||
|
||||
return {
|
||||
...query,
|
||||
hasPets: Boolean(query.data?.length),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Подробное потребление таких хуков описано в стратегии [Клиентский GET-хук](../../data-fetch/client-get-hook.md).
|
||||
88
ai/nextjs-style-guide/applied/rest-client/setup/index.md
Normal file
88
ai/nextjs-style-guide/applied/rest-client/setup/index.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: Настройка REST-клиента
|
||||
description: Подготовка REST-клиента сервиса к использованию.
|
||||
keywords: [rest, клиент, infra, методы, openapi, get-хуки, swr]
|
||||
---
|
||||
|
||||
# Настройка REST-клиента
|
||||
|
||||
Подготовка REST-клиента сервиса к использованию.
|
||||
|
||||
## Что настраиваем
|
||||
|
||||
REST-клиент — это infra-модуль, через который проект работает с внешним REST API.
|
||||
|
||||
На этапе настройки нужно подготовить клиент сервиса: оболочку клиента, методы API и GET-хуки для клиентских компонентов.
|
||||
|
||||
## Из чего состоит клиент
|
||||
|
||||
REST-клиент состоит из трёх основных частей:
|
||||
|
||||
1. **Клиент** — самописная оболочка над транспортом.
|
||||
2. **Методы** — сгенерированные из OpenAPI или написанные вручную вызовы API.
|
||||
3. **GET-хуки** — SWR-обёртки для GET-запросов.
|
||||
|
||||
Эти части живут в одном REST-модуле, потому что относятся к одному внешнему сервису.
|
||||
|
||||
## Клиент
|
||||
|
||||
Клиент — ручной слой, который настраивает работу с API: `baseUrl`, заголовки, авторизацию, обработку ошибок и создание инстанса сервиса.
|
||||
|
||||
Даже если методы генерируются из OpenAPI, `client.ts` остаётся ручным файлом проекта.
|
||||
|
||||
`client.ts` — только сборочная точка клиента. В нём не размещаются DTO, `declare module`, `Extended`-типы, GET-хуки и бизнес-логика.
|
||||
|
||||
`baseUrl` API задаётся обязательной env-переменной без fallback-значения в коде. Не используйте записи вроде `process.env.NEXT_PUBLIC_PET_STORE_API_BASE_URL ?? 'http://localhost:8080/api/v3'` или `?? ''`: локальный URL должен лежать в `.env.local`, а отсутствие переменной должно приводить к явной ошибке конфигурации.
|
||||
|
||||
## Методы
|
||||
|
||||
Методы описывают конкретные запросы к API.
|
||||
|
||||
Они появляются одним из двух способов:
|
||||
|
||||
- генерируются из OpenAPI в `generated/`;
|
||||
- создаются вручную в `methods/`.
|
||||
|
||||
Подробности:
|
||||
|
||||
- [Автогенерация из OpenAPI](./auto.md)
|
||||
- [Ручное создание](./manual.md)
|
||||
|
||||
## GET-хуки
|
||||
|
||||
Для GET-запросов добавляются GET-хуки REST-клиента.
|
||||
|
||||
Это прозрачные SWR-обёртки над GET-методами клиента. Они живут в `hooks/` этого же REST-модуля и нужны для использования данных в Client Components.
|
||||
|
||||
GET-хуки именуются с префиксом `useGet`: `useGetPetList`, `useGetPetDetail`, `useGetCurrentUser`.
|
||||
|
||||
Каждый GET-хук имеет экспортируемую key-функцию. SWR-ключ всегда имеет формат `[serviceName, endpoint]`: например `['pet-store-api', '/pet/10']`.
|
||||
|
||||
Хук принимает generated-параметры метода и SWR-настройки: `params?: GetPetByIdParams | null`, `config?: SWRConfiguration<Pet>`.
|
||||
|
||||
Подробности:
|
||||
|
||||
- [GET-хуки REST-клиента](./hooks.md)
|
||||
|
||||
## Структура модуля
|
||||
|
||||
```text
|
||||
src/infra/{service-name}/
|
||||
├── client.ts # самописная оболочка и инстанс клиента
|
||||
├── generated/ или methods/ # методы API
|
||||
├── hooks/ # GET-хуки REST-клиента
|
||||
├── types/ # DTO, именованные response-типы и расширения типов
|
||||
├── errors/ # ошибки API, если нужны
|
||||
└── index.ts # публичный API
|
||||
```
|
||||
|
||||
`index.ts` — единственная точка входа в REST-модуль для внешнего кода.
|
||||
|
||||
Если generated-метод возвращает безымянный тип вроде `Record<string, number>`, а этот тип нужен снаружи, вынесите его в `types/`. Не объявляйте DTO внутри `hooks/use-get-*.hook.ts`.
|
||||
|
||||
## Что делаем дальше
|
||||
|
||||
1. Создайте методы клиента: [Автогенерация из OpenAPI](./auto.md) или [Ручное создание](./manual.md).
|
||||
2. Добавьте GET-хуки для GET-запросов: [GET-хуки REST-клиента](./hooks.md).
|
||||
3. Проверьте прямые вызовы клиента: [Использование REST-клиента](../usage.md).
|
||||
4. После настройки клиента переходите к [Получению данных](../../data-fetch/index.md).
|
||||
198
ai/nextjs-style-guide/applied/rest-client/setup/manual.md
Normal file
198
ai/nextjs-style-guide/applied/rest-client/setup/manual.md
Normal file
@@ -0,0 +1,198 @@
|
||||
---
|
||||
title: Ручное создание REST-клиента
|
||||
description: Создание REST-клиента вручную, когда OpenAPI нет или он неполный.
|
||||
keywords: [rest, ручной клиент, fetch, methods, dto, errors, infra]
|
||||
---
|
||||
|
||||
# Ручное создание REST-клиента
|
||||
|
||||
Создание REST-клиента вручную, когда OpenAPI нет или он неполный.
|
||||
|
||||
## Когда использовать
|
||||
|
||||
Ручной REST-клиент используется, когда у API нет OpenAPI-спецификации или она недостаточно точная для автогенерации.
|
||||
|
||||
Задача ручного клиента — дать такую же точку входа, как у автогенерированного клиента: именованный API-объект, методы по сущностям, DTO и GET-хуки рядом с клиентом.
|
||||
|
||||
## Что нужно создать
|
||||
|
||||
```text
|
||||
src/infra/
|
||||
└── pet-project-api/
|
||||
├── methods/
|
||||
│ └── posts.ts
|
||||
├── hooks/
|
||||
│ └── index.ts
|
||||
├── types/
|
||||
│ ├── client.ts
|
||||
│ ├── post.ts
|
||||
│ └── index.ts
|
||||
├── errors/
|
||||
│ └── pet-project-api.error.ts
|
||||
├── client.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
| Файл | Роль |
|
||||
|------|------|
|
||||
| `client.ts` | Базовый транспорт и создание инстанса клиента |
|
||||
| `methods/` | Методы API по сущностям |
|
||||
| `types/` | DTO запросов, ответов и типы клиента |
|
||||
| `errors/` | Ошибки конкретного API |
|
||||
| `hooks/` | GET-хуки REST-клиента, если данные нужны в Client Components |
|
||||
| `index.ts` | Публичный API REST-модуля |
|
||||
|
||||
## DTO и типы API
|
||||
|
||||
DTO запросов и ответов живут в `types/`. `client.ts` не содержит DTO и доменные типы.
|
||||
|
||||
```ts
|
||||
// src/infra/pet-project-api/types/post.ts
|
||||
export type PostDto = {
|
||||
id: string
|
||||
slug: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export type PostListQueryDto = {
|
||||
limit?: number
|
||||
category?: string
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/infra/pet-project-api/types/index.ts
|
||||
export type { PostDto, PostListQueryDto } from './post'
|
||||
```
|
||||
|
||||
Типы, которые нужны только базовому транспорту, можно держать отдельно:
|
||||
|
||||
```ts
|
||||
// src/infra/pet-project-api/types/client.ts
|
||||
export type QueryParams = Record<string, string | number | boolean>
|
||||
```
|
||||
|
||||
## Ошибка API
|
||||
|
||||
Ошибка API тоже относится к REST-модулю.
|
||||
|
||||
```ts
|
||||
// src/infra/pet-project-api/errors/pet-project-api.error.ts
|
||||
export class PetProjectApiError extends Error {
|
||||
constructor(
|
||||
public readonly status: number,
|
||||
message: string,
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'PetProjectApiError'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Базовый клиент
|
||||
|
||||
`client.ts` содержит только транспортную оболочку и сборку инстанса. Прямой `fetch` живёт здесь, а не в компонентах и не в методах верхних слоёв.
|
||||
|
||||
```ts
|
||||
// src/infra/pet-project-api/client.ts
|
||||
import { PetProjectApiError } from './errors/pet-project-api.error'
|
||||
import type { QueryParams } from './types/client'
|
||||
|
||||
export class PetProjectApiClient {
|
||||
constructor(
|
||||
private readonly baseUrl: string,
|
||||
private readonly defaultHeaders: Record<string, string> = {},
|
||||
) {}
|
||||
|
||||
async get<T>(path: string, params: QueryParams = {}): Promise<T> {
|
||||
const base = `${this.baseUrl.replace(/\/+$/, '')}/`
|
||||
const url = new URL(path.replace(/^\/+/, ''), base)
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
url.searchParams.set(key, String(value))
|
||||
})
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...this.defaultHeaders,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new PetProjectApiError(response.status, response.statusText)
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Это минимальный шаблон. Авторизация, дополнительные заголовки, `next.revalidate`, `post`, `formdata` и другие детали добавляются только когда они реально нужны API.
|
||||
|
||||
## Методы API
|
||||
|
||||
Методы группируются по сущностям в `methods/`. Они не знают про React, SWR и UI.
|
||||
|
||||
```ts
|
||||
// src/infra/pet-project-api/methods/posts.ts
|
||||
import type { PetProjectApiClient } from '../client'
|
||||
import type { PostDto, PostListQueryDto } from '../types/post'
|
||||
|
||||
export function postsMethods(client: PetProjectApiClient) {
|
||||
return {
|
||||
/** GET /posts */
|
||||
list: (query: PostListQueryDto = {}) =>
|
||||
client.get<PostDto[]>('posts', query),
|
||||
|
||||
/** GET /posts/{slug} */
|
||||
get: (slug: string) =>
|
||||
client.get<PostDto>(`posts/${slug}`),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Метод возвращает DTO в форме API. Если данным нужен доменный смысл, маппинг делается выше, в `business/`.
|
||||
|
||||
## Публичный API
|
||||
|
||||
`index.ts` собирает именованный API-объект и открывает наружу только публичные части модуля.
|
||||
|
||||
```ts
|
||||
// src/infra/pet-project-api/index.ts
|
||||
import { PetProjectApiClient } from './client'
|
||||
import { postsMethods } from './methods/posts'
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_PET_PROJECT_API_BASE_URL
|
||||
|
||||
if (!baseUrl) {
|
||||
throw new Error('NEXT_PUBLIC_PET_PROJECT_API_BASE_URL is required')
|
||||
}
|
||||
|
||||
const client = new PetProjectApiClient(
|
||||
baseUrl,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
)
|
||||
|
||||
export const petProjectApi = {
|
||||
posts: postsMethods(client),
|
||||
}
|
||||
|
||||
export { PetProjectApiError } from './errors/pet-project-api.error'
|
||||
export type { PostDto, PostListQueryDto } from './types'
|
||||
export * from './hooks'
|
||||
```
|
||||
|
||||
Внешний код импортирует только из `infra/pet-project-api`, не из внутренних файлов модуля.
|
||||
|
||||
## Правила
|
||||
|
||||
- `fetch` используется только внутри базового клиента.
|
||||
- DTO запросов и ответов живут в `types/`.
|
||||
- `client.ts` не содержит DTO, GET-хуки и бизнес-логику.
|
||||
- `baseUrl` берётся из обязательной env-переменной без fallback-значения в коде.
|
||||
- Методы лежат в `methods/` и возвращают DTO.
|
||||
- GET-хуки добавляются отдельно в `hooks/`, если данные нужны в Client Components.
|
||||
- Доменные типы и маппинг DTO живут не в REST-клиенте, а в `business/`.
|
||||
|
||||
Следующий шаг: [Использование REST-клиента](../usage.md), [GET-хуки REST-клиента](./hooks.md) или [Получение данных](../../data-fetch/index.md).
|
||||
Reference in New Issue
Block a user