forked from templates/nextjs-template
style: Обновление код стайла
This commit is contained in:
@@ -19,35 +19,28 @@
|
||||
- [Документирование](./basics/documentation.md) — Что и как документировать в коде.
|
||||
- [Типизация](./basics/typing.md) — Как типизируется код в проекте.
|
||||
|
||||
## Создание проекта
|
||||
|
||||
- [Из шаблона](./creating-project/from-template.md) — Создание нового проекта на основе готового шаблона.
|
||||
- [По гайду вручную](./creating-project/manual.md) — Поэтапное создание нового проекта без использования шаблона.
|
||||
- [Чистый Next.js](./creating-project/nextjs.md) — Установка Next.js без лишнего шаблона — голый каркас под дальнейшую сборку.
|
||||
|
||||
## Работа с данными
|
||||
|
||||
- [Введение](./data/index.md) — Какие источники данных используются в проекте и как с ними работать.
|
||||
- [REST](./data/rest/index.md) — Как правильно работать с REST API в проекте.
|
||||
- [REST: Создание клиента](./data/rest/clients/index.md) — Как выбрать способ создания REST-клиента и где размещать его части.
|
||||
- [REST: Создание клиента: Автогенерация из OpenAPI](./data/rest/clients/auto.md) — Генерация REST-клиента из OpenAPI-спецификации через @gromlab/api-codegen.
|
||||
- [REST: Создание клиента: Ручное создание](./data/rest/clients/manual.md) — Создание REST-клиента вручную, когда OpenAPI нет или он неполный.
|
||||
- [REST: Создание клиента: GET-хуки REST-клиента](./data/rest/clients/hooks.md) — Прозрачные SWR-обёртки над GET-методами REST-клиента.
|
||||
- [REST: Использование: Стратегии получения данных](./data/rest/strategies/index.md) — Как выбрать способ получения REST-данных в зависимости от места и сценария.
|
||||
- [REST: Использование: Серверный await](./data/rest/strategies/server-await.md) — Получение REST-данных на сервере прямым await метода клиента.
|
||||
- [REST: Использование: Параллельные серверные запросы](./data/rest/strategies/parallel-server-requests.md) — Как запускать независимые REST-запросы на сервере без waterfall.
|
||||
- [REST: Использование: Передача промиса ниже](./data/rest/strategies/pass-promise-down.md) — Как запускать серверный REST-запрос выше и ожидать его во вложенном server-компоненте.
|
||||
- [REST: Использование: Начальные данные для клиентских хуков](./data/rest/strategies/client-hooks-initial-data.md) — Как передать серверный промис в SWR fallback, чтобы клиентские GET-хуки получили начальные данные.
|
||||
- [REST: Использование: Клиентский GET-хук](./data/rest/strategies/client-get-hook.md) — Получение REST-данных в Client Components через готовые GET-хуки REST-клиента.
|
||||
- [REST: Использование: Business-композиция](./data/rest/strategies/business-composition.md) — Когда REST-данные нужно объединить или интерпретировать в бизнес-модуле.
|
||||
- [Realtime](./data/realtime.md) — Работа с push-данными от сервера: подписки и события.
|
||||
|
||||
## Прикладные разделы
|
||||
|
||||
- [Создание проекта: Из шаблона](./applied/creating-project/from-template.md) — Создание нового проекта на основе готового шаблона.
|
||||
- [Создание проекта: По гайду вручную](./applied/creating-project/manual.md) — Поэтапное создание нового проекта без использования шаблона.
|
||||
- [Создание проекта: Чистый Next.js](./applied/creating-project/nextjs.md) — Установка Next.js без лишнего шаблона — голый каркас под дальнейшую сборку.
|
||||
- [Структура проекта](./applied/project-structure.md) — Из чего состоит проект и где что лежит.
|
||||
- [Страницы](./applied/page-level.md) — Как работать со страницами и другими файлами роутинга Next.js App Router.
|
||||
- [Компонент](./applied/component.md) — Как создавать React-компоненты внутри SLM-модулей.
|
||||
- [Модуль](./applied/module.md) — Как создавать и организовывать SLM-модули в проекте.
|
||||
- [REST-клиент](./applied/rest-client/index.md) — Настройка REST-клиента сервиса для работы с внешним API.
|
||||
- [REST-клиент: Настройка REST-клиента](./applied/rest-client/setup/index.md) — Из чего состоит REST-клиент и что подготовить перед использованием API.
|
||||
- [REST-клиент: Автогенерация REST-клиента](./applied/rest-client/setup/auto.md) — Генерация REST-клиента из OpenAPI-спецификации.
|
||||
- [REST-клиент: Ручное создание REST-клиента](./applied/rest-client/setup/manual.md) — Создание REST-клиента вручную, когда OpenAPI нет или он неполный.
|
||||
- [REST-клиент: GET-хуки REST-клиента](./applied/rest-client/setup/hooks.md) — Прозрачные SWR-обёртки над GET-методами REST-клиента.
|
||||
- [REST-клиент: Использование REST-клиента](./applied/rest-client/usage.md) — Как вызывать готовый REST-клиент в серверном коде и submit-функциях.
|
||||
- [Получение данных](./applied/data-fetch/index.md) — Как получать данные с учётом рендера страницы.
|
||||
- [Получение данных: Серверный await](./applied/data-fetch/server-await.md) — Получение REST-данных на сервере до первого HTML.
|
||||
- [Получение данных: Параллельные серверные запросы](./applied/data-fetch/parallel-server-requests.md) — Как запускать независимые REST-запросы на сервере без waterfall.
|
||||
- [Получение данных: Передача промиса ниже](./applied/data-fetch/pass-promise-down.md) — Как запускать серверный REST-запрос выше и ожидать его во вложенном server-компоненте.
|
||||
- [Получение данных: Начальные данные для клиентских хуков](./applied/data-fetch/client-hooks-initial-data.md) — Как дать клиентским GET-хукам начальные REST-данные.
|
||||
- [Получение данных: Клиентский GET-хук](./applied/data-fetch/client-get-hook.md) — Получение REST-данных в Client Components через готовые GET-хуки REST-клиента.
|
||||
- [Получение данных: Business-композиция](./applied/data-fetch/business-composition.md) — Когда REST-данные нужно объединить или интерпретировать в бизнес-модуле.
|
||||
- [Стили: Настройка](./applied/styles/styles-setup.md) — Подготовка стилевой основы проекта: токены, медиа-запросы, глобальные стили.
|
||||
- [Стили: Использование](./applied/styles/styles-usage.md) — Как пишутся стили в проекте.
|
||||
- [SVG-спрайты](./applied/svg-sprites/svg-sprites-intro.md) — Что такое SVG-спрайты и какие проблемы они решают.
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
eadc462
|
||||
2026-05-08T04:14:35.127Z
|
||||
8231356
|
||||
2026-05-08T16:14:00.876Z
|
||||
|
||||
@@ -29,7 +29,7 @@ keywords: [biome, линтер, форматтер, lint, format, biome.json, "@
|
||||
|
||||
В корне появится `biome.json` с дефолтными настройками.
|
||||
|
||||
3. Привести `biome.json` к стандартному виду — добавить override для `*.css` (см. «Стандартный `biome.json`»). Делается сразу после `init`, до первого запуска `lint`/`check`.
|
||||
3. Привести `biome.json` к стандартному виду (см. «Стандартный `biome.json`»). Делается сразу после `init`, до первого запуска `lint`/`check`.
|
||||
|
||||
4. Добавить скрипты в `package.json`:
|
||||
|
||||
@@ -51,30 +51,63 @@ keywords: [biome, линтер, форматтер, lint, format, biome.json, "@
|
||||
|
||||
## Стандартный `biome.json`
|
||||
|
||||
Дефолтный `biome.json`, созданный `biome init`, кастомизируется ровно одним блоком — `overrides` для `*.css` с отключённым правилом `suspicious/noUnknownAtRules`. Этот override **обязателен по умолчанию во всех проектах**, независимо от того, подключены ли уже стили: проектный CSS-стек использует `@custom-media` и другие нестандартные at-правила, которые Biome не распознаёт; без override `npm run lint` падает.
|
||||
Дефолтный `biome.json`, созданный `biome init`, заменяется стандартным конфигом проекта.
|
||||
|
||||
Фрагмент, который добавляется в `biome.json`:
|
||||
Стандартный `biome.json`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"overrides": [
|
||||
{
|
||||
"includes": ["**/*.css"],
|
||||
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": true,
|
||||
"includes": ["**", "!node_modules", "!.next", "!dist", "!build", "!.templates", "!src/infra/**/generated"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 120
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "single",
|
||||
"jsxQuoteStyle": "double"
|
||||
}
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"suspicious": {
|
||||
"noUnknownAtRules": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"noUnknownMediaFeatureName": "off"
|
||||
}
|
||||
},
|
||||
"domains": {
|
||||
"next": "recommended",
|
||||
"react": "recommended"
|
||||
}
|
||||
},
|
||||
"assist": {
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": "on"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Если в `biome.json` уже есть массив `overrides` — добавить элемент в него; не дублировать массив.
|
||||
`src/infra/**/generated` исключается из Biome, потому что generated-файлы не правятся руками. При этом generated-файлы остаются в git.
|
||||
|
||||
Прочая настройка правил Biome — отдельная задача, не входит в стандартный канон.
|
||||
Правила `suspicious/noUnknownAtRules` и `correctness/noUnknownMediaFeatureName` отключены, потому что проектный CSS-стек использует `@custom-media` и другие конструкции, которые Biome может не распознавать.
|
||||
|
||||
## Интеграция с VS Code
|
||||
|
||||
|
||||
@@ -14,11 +14,11 @@ keywords: [создать проект из шаблона, шаблон, templa
|
||||
|
||||
- **Стек:** Next.js (App Router), TypeScript, React.
|
||||
- **Архитектура:** структура папок по SLM, алиасы импортов.
|
||||
- **Качество кода:** Biome (линтер и форматер), настройки VS Code.
|
||||
- **Качество кода:** Biome (линтер и форматтер), настройки VS Code.
|
||||
- **Стили:** PostCSS Modules с плагинами, токены, медиа-брейкпоинты.
|
||||
- **Ассеты:** генерация SVG-спрайтов.
|
||||
- **Кодогенерация:** шаблоны для страниц, компонентов, хуков, сторов.
|
||||
в
|
||||
|
||||
## Установка
|
||||
|
||||
1. Склонировать шаблон в родительском каталоге будущего проекта:
|
||||
@@ -13,19 +13,19 @@ keywords: [создать проект, новый проект, с нуля, in
|
||||
| Компонент | Роль | Раздел |
|
||||
|-----------|------|--------|
|
||||
| Next.js | Фреймворк (роутинг, сборка, SSR) | [Next.js](./nextjs.md) |
|
||||
| Алиасы | Импорты по слоям SLM | [Алиасы](../applied/aliases.md) |
|
||||
| Biome | Линтер и форматтер (замена ESLint + Prettier) | [Biome](../applied/biome.md) |
|
||||
| Стили | Глобальные токены и breakpoints | [Стили](../applied/styles/styles-setup.md) |
|
||||
| PostCSS | CSS-процессор для custom-media и вложенности | [PostCSS](../applied/postcss.md) |
|
||||
| SVG-спрайты | Иконки через `<SvgSprite/>`, управление цветом | [SVG-спрайты](../applied/svg-sprites/svg-sprites-setup.md) |
|
||||
| VS Code | Настройки редактора и расширения | [VS Code](../applied/vscode.md) |
|
||||
| Шаблоны генерации | `.templates/` для `@gromlab/create` | [Шаблоны генерации](../applied/templates/templates-setup.md) |
|
||||
| Алиасы | Импорты по слоям SLM | [Алиасы](../aliases.md) |
|
||||
| Biome | Линтер и форматтер (замена ESLint + Prettier) | [Biome](../biome.md) |
|
||||
| Стили | Глобальные токены и breakpoints | [Стили](../styles/styles-setup.md) |
|
||||
| PostCSS | CSS-процессор для custom-media и вложенности | [PostCSS](../postcss.md) |
|
||||
| SVG-спрайты | Иконки через `<SvgSprite/>`, управление цветом | [SVG-спрайты](../svg-sprites/svg-sprites-setup.md) |
|
||||
| VS Code | Настройки редактора и расширения | [VS Code](../vscode.md) |
|
||||
| Шаблоны генерации | `.templates/` для `@gromlab/create` | [Шаблоны генерации](../templates/templates-setup.md) |
|
||||
|
||||
Убрать компонент из состава — значит согласованно отказаться от части стайлгайда. Частичные проекты возможны (только Next.js, Next.js + стили и т.п.), но не являются эталоном.
|
||||
|
||||
## Канон раскладки
|
||||
|
||||
В `src/` допустимы только слои SLM: `app/`, `layouts/`, `screens/`, `widgets/`, `business/`, `infra/`, `ui/`, `shared/`. Любая другая папка в `src/` — нарушение канона ([Структура проекта](../applied/project-structure.md), [Архитектура](../basics/architecture/index.md)).
|
||||
В `src/` допустимы только слои SLM: `app/`, `layouts/`, `screens/`, `widgets/`, `business/`, `infra/`, `ui/`, `shared/`. Любая другая папка в `src/` — нарушение канона ([Структура проекта](../project-structure.md), [Архитектура](../../basics/architecture/index.md)).
|
||||
|
||||
В частности: `src/app/` содержит только файлы роутинга Next.js и инициализации, без каталогов `styles/`, `assets/`, `components/`.
|
||||
|
||||
@@ -43,43 +43,43 @@ keywords: [создать проект, новый проект, с нуля, in
|
||||
|
||||
Заменить дефолтный `"@/*"` в `tsconfig.json` на канонический список из восьми слой-префиксов.
|
||||
|
||||
См. [Алиасы](../applied/aliases.md).
|
||||
См. [Алиасы](../aliases.md).
|
||||
|
||||
### 3. Biome
|
||||
|
||||
Линтер и форматтер. Подключается **до** написания кода, иначе в проекте копятся несогласованные правки.
|
||||
|
||||
См. [Biome](../applied/biome.md).
|
||||
См. [Biome](../biome.md).
|
||||
|
||||
### 4. Стили (базовая инфраструктура)
|
||||
|
||||
Файлы `variables.css`, `media.css`, `global.css` в `src/shared/styles/` и подключение `global.css` в `src/app/layout.tsx`. CSS-процессор на этом шаге не ставится.
|
||||
|
||||
См. [Стили](../applied/styles/styles-setup.md).
|
||||
См. [Стили](../styles/styles-setup.md).
|
||||
|
||||
### 5. PostCSS
|
||||
|
||||
CSS-процессор поверх базовых стилей: `@custom-media`, вложенность, autoprefixer. Ставится **только после шага 4** — опирается на `src/shared/styles/media.css`.
|
||||
|
||||
См. [PostCSS](../applied/postcss.md).
|
||||
См. [PostCSS](../postcss.md).
|
||||
|
||||
### 6. SVG-спрайты
|
||||
|
||||
Пакет `@gromlab/svg-sprites`, генерация спрайт-файла и React-компонента `<SvgSprite/>`.
|
||||
|
||||
См. [SVG-спрайты](../applied/svg-sprites/svg-sprites-setup.md).
|
||||
См. [SVG-спрайты](../svg-sprites/svg-sprites-setup.md).
|
||||
|
||||
### 7. VS Code
|
||||
|
||||
Расширения и настройки редактора. Опирается на установленный Biome (форматирование при сохранении) и PostCSS (ассоциация `*.css`).
|
||||
|
||||
См. [VS Code](../applied/vscode.md).
|
||||
См. [VS Code](../vscode.md).
|
||||
|
||||
### 8. Шаблоны генерации
|
||||
|
||||
Папка `.templates/` для генератора модулей `@gromlab/create`.
|
||||
|
||||
См. [Шаблоны генерации](../applied/templates/templates-setup.md).
|
||||
См. [Шаблоны генерации](../templates/templates-setup.md).
|
||||
|
||||
## Правила
|
||||
|
||||
@@ -33,12 +33,12 @@ npx create-next-app@latest my-app \
|
||||
|
||||
| Флаг | Значение | Почему так |
|
||||
|------|----------|------------|
|
||||
| `--typescript` | TS включён | Стайлгайд требует TypeScript ([Типизация](../basics/typing.md)) |
|
||||
| `--typescript` | TS включён | Стайлгайд требует TypeScript ([Типизация](../../basics/typing.md)) |
|
||||
| `--app` | App Router | Pages Router не используется |
|
||||
| `--src-dir` | Код в `src/` | Архитектура SLM требует `src/` ([Структура проекта](../applied/project-structure.md)) |
|
||||
| `--import-alias "@/*"` | Placeholder | Требуется флагом; после установки `paths` полностью переписывается на слой-префиксы (см. [Алиасы](../applied/aliases.md)) |
|
||||
| `--no-eslint` | ESLint не ставится | Линтер и форматтер — Biome ([Biome](../applied/biome.md)) |
|
||||
| `--no-tailwind` | Tailwind не ставится | Стилизация — PostCSS Modules ([Стили](../applied/styles/styles-usage.md)) |
|
||||
| `--src-dir` | Код в `src/` | Архитектура SLM требует `src/` ([Структура проекта](../project-structure.md)) |
|
||||
| `--import-alias "@/*"` | Placeholder | Требуется флагом; после установки `paths` полностью переписывается на слой-префиксы (см. [Алиасы](../aliases.md)) |
|
||||
| `--no-eslint` | ESLint не ставится | Линтер и форматтер — Biome ([Biome](../biome.md)) |
|
||||
| `--no-tailwind` | Tailwind не ставится | Стилизация — PostCSS Modules ([Стили](../styles/styles-usage.md)) |
|
||||
| `--use-npm` | Пакетный менеджер — npm | Единый инструмент в проектах |
|
||||
|
||||
### 2. Очистить дефолтный шаблон
|
||||
@@ -83,7 +83,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
|
||||
### 3. Создать папку `src/shared/styles/`
|
||||
|
||||
Глобальные стили в SLM-архитектуре живут в слое `shared`, а не в `src/app/` ([Структура проекта](../applied/project-structure.md)).
|
||||
Глобальные стили в SLM-архитектуре живут в слое `shared`, а не в `src/app/` ([Структура проекта](../project-structure.md)).
|
||||
|
||||
```bash
|
||||
mkdir -p src/shared/styles
|
||||
@@ -95,7 +95,7 @@ mkdir -p src/shared/styles
|
||||
|
||||
- **Конфликт с непустой директорией** — не удалять файлы пользователя автоматически. Ставить в подпапку или временно перенести посторонние файлы.
|
||||
- **Отклонение от канонических флагов** (pnpm, Tailwind, ESLint и т.п.) — только осознанное, с пониманием, что стайлгайд этого не предусматривает.
|
||||
- **Слои `src/`** не создавать авансом — появляются при первом модуле. Алиасы прописываются сразу на все восемь слоёв (см. [Алиасы](../applied/aliases.md)).
|
||||
- **Слои `src/`** не создавать авансом — появляются при первом модуле. Алиасы прописываются сразу на все восемь слоёв (см. [Алиасы](../aliases.md)).
|
||||
- **`AGENTS.md` от Next.js** удаляется в шаге 2. Повторно не создаётся.
|
||||
- **Biome, стили, PostCSS, SVG-спрайты, VS Code** — отдельные шаги установки, не в этом разделе.
|
||||
|
||||
@@ -21,13 +21,13 @@ Business-композиция используется, когда просто
|
||||
|
||||
```ts
|
||||
// src/business/pets/hooks/use-available-pets.hook.ts
|
||||
import { useGetPetList } from 'infra/pet-store-api'
|
||||
import { StatusEnum, useGetPetList } from 'infra/pet-store-api'
|
||||
|
||||
/**
|
||||
* Доменный список доступных питомцев.
|
||||
*/
|
||||
export const useAvailablePets = () => {
|
||||
const query = useGetPetList('available')
|
||||
const query = useGetPetList({ status: StatusEnum.Available })
|
||||
|
||||
return {
|
||||
...query,
|
||||
@@ -42,15 +42,15 @@ export const useAvailablePets = () => {
|
||||
|
||||
```ts
|
||||
// src/business/pets/hooks/use-pets-dashboard.hook.ts
|
||||
import { useGetPetList } from 'infra/pet-store-api'
|
||||
import { StatusEnum, useGetPetList } from 'infra/pet-store-api'
|
||||
|
||||
/**
|
||||
* Данные dashboard по питомцам.
|
||||
*/
|
||||
export const usePetsDashboard = () => {
|
||||
const availablePets = useGetPetList('available')
|
||||
const pendingPets = useGetPetList('pending')
|
||||
const soldPets = useGetPetList('sold')
|
||||
const availablePets = useGetPetList({ status: StatusEnum.Available })
|
||||
const pendingPets = useGetPetList({ status: StatusEnum.Pending })
|
||||
const soldPets = useGetPetList({ status: StatusEnum.Sold })
|
||||
|
||||
return {
|
||||
availablePets,
|
||||
@@ -108,7 +108,7 @@ src/business/
|
||||
|
||||
```ts
|
||||
// Плохо — business-смысл внутри infra-хука
|
||||
export const useGetPetList = (status: PetStatus) => {
|
||||
export const useGetPetList = (params?: FindPetsByStatusParams | null) => {
|
||||
const query = useSWR(...)
|
||||
|
||||
return {
|
||||
@@ -21,14 +21,13 @@ keywords: [rest, client components, swr, get-хук, client state]
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useGetPetList } from 'infra/pet-store-api'
|
||||
import type { PetStatus } from 'infra/pet-store-api'
|
||||
import { StatusEnum, useGetPetList } from 'infra/pet-store-api'
|
||||
|
||||
const statuses: PetStatus[] = ['available', 'pending', 'sold']
|
||||
const statuses = [StatusEnum.Available, StatusEnum.Pending, StatusEnum.Sold]
|
||||
|
||||
export function PetTabs() {
|
||||
const [status, setStatus] = useState<PetStatus>('available')
|
||||
const { data: pets, isLoading, error } = useGetPetList(status)
|
||||
const [status, setStatus] = useState(StatusEnum.Available)
|
||||
const { data: pets, isLoading, error } = useGetPetList({ status })
|
||||
|
||||
return (
|
||||
<section>
|
||||
@@ -75,7 +74,7 @@ useEffect(() => {
|
||||
|
||||
// Плохо — useSWR в компоненте
|
||||
const { data } = useSWR(
|
||||
['pet-store-api', 'pet', 'list', status],
|
||||
['pet-store-api', `/pet/findByStatus?status=${status}`],
|
||||
() => petStoreApi.pet.findPetsByStatus({ status }),
|
||||
)
|
||||
```
|
||||
@@ -31,8 +31,13 @@ keywords: [rest, swr, fallback, initial data, client hooks, unstable_serialize,
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-api/hooks/use-get-pet-list.hook.ts
|
||||
export const getPetListKey = (status: PetStatus) =>
|
||||
['pet-store-api', 'pet', 'list', status] as const
|
||||
export const getPetListKey = (params?: FindPetsByStatusParams | null) => {
|
||||
if (!params?.status) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ['pet-store-api', `/pet/findByStatus?status=${params.status}`] as const
|
||||
}
|
||||
```
|
||||
|
||||
Ключ экспортируется из REST-модуля, потому что он нужен и GET-хуку, и серверному `SWRConfig fallback`.
|
||||
@@ -46,6 +51,7 @@ import { SWRConfig, unstable_serialize } from 'swr'
|
||||
import {
|
||||
getPetListKey,
|
||||
petStoreApi,
|
||||
StatusEnum,
|
||||
} from 'infra/pet-store-api'
|
||||
|
||||
type PetsLayoutProps = {
|
||||
@@ -53,15 +59,14 @@ type PetsLayoutProps = {
|
||||
}
|
||||
|
||||
export default async function PetsLayout({ children }: PetsLayoutProps) {
|
||||
const availablePetsPromise = petStoreApi.pet.findPetsByStatus({
|
||||
status: 'available',
|
||||
})
|
||||
const params = { status: StatusEnum.Available }
|
||||
const availablePetsPromise = petStoreApi.pet.findPetsByStatus(params)
|
||||
|
||||
return (
|
||||
<SWRConfig
|
||||
value={{
|
||||
fallback: {
|
||||
[unstable_serialize(getPetListKey('available'))]: availablePetsPromise,
|
||||
[unstable_serialize(getPetListKey(params))]: availablePetsPromise,
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -78,10 +83,12 @@ export default async function PetsLayout({ children }: PetsLayoutProps) {
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { useGetPetList } from 'infra/pet-store-api'
|
||||
import { StatusEnum, useGetPetList } from 'infra/pet-store-api'
|
||||
|
||||
export function PetList() {
|
||||
const { data: pets, isLoading } = useGetPetList('available')
|
||||
const { data: pets, isLoading } = useGetPetList({
|
||||
status: StatusEnum.Available,
|
||||
})
|
||||
|
||||
if (isLoading) return <div>Загрузка...</div>
|
||||
|
||||
@@ -100,6 +107,7 @@ export function PetList() {
|
||||
## Что важно
|
||||
|
||||
- Ключ `fallback` должен совпадать с ключом GET-хука.
|
||||
- `fallback` использует ту же key-функцию и те же params, что и GET-хук.
|
||||
- Серверный код вызывает метод клиента, а не GET-хук.
|
||||
- Клиентский компонент вызывает GET-хук, а не `useSWR` напрямую.
|
||||
- Эта стратегия не означает ручную работу с кешем в компонентах.
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
title: Стратегии получения данных
|
||||
description: Как выбрать получение REST-данных с учётом рендера страницы.
|
||||
title: Получение данных
|
||||
description: Как получать данные с учётом рендера страницы.
|
||||
keywords: [rest, стратегии, render, isr, ssr, server components, swr, promise, business]
|
||||
---
|
||||
|
||||
# Стратегии получения данных
|
||||
# Получение данных
|
||||
|
||||
Как выбрать получение REST-данных с учётом рендера страницы.
|
||||
Как получать данные с учётом рендера страницы.
|
||||
|
||||
Перед выбором стратегии должен быть создан REST-клиент сервиса. Если клиента ещё нет, начните с раздела [Создание клиента](../clients/index.md).
|
||||
Перед выбором стратегии должен быть настроен REST-клиент сервиса. Если клиента ещё нет, начните с раздела [Настройка REST-клиента](../rest-client/setup/index.md).
|
||||
|
||||
## Сначала определите рендер страницы
|
||||
|
||||
@@ -86,8 +86,8 @@ useEffect(() => {
|
||||
|
||||
// Плохо — useSWR в компоненте
|
||||
const { data } = useSWR(
|
||||
['pet-store-api', 'pet', 'list', status],
|
||||
() => petStoreApi.pet.findPetsByStatus({ status }),
|
||||
['pet-store-api', '/pet/findByStatus?status=available'],
|
||||
() => petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Available }),
|
||||
)
|
||||
|
||||
// Плохо — бизнес-флаг внутри GET-хука REST-клиента
|
||||
@@ -17,13 +17,19 @@ keywords: [rest, promise.all, параллельные запросы, server co
|
||||
## Хорошо
|
||||
|
||||
```tsx
|
||||
import { petStoreApi } from 'infra/pet-store-api'
|
||||
import { petStoreApi, StatusEnum } from 'infra/pet-store-api'
|
||||
import { PetsDashboardScreen } from 'screens/pets-dashboard'
|
||||
|
||||
export default async function PetsDashboardPage() {
|
||||
const availablePetsPromise = petStoreApi.pet.findPetsByStatus({ status: 'available' })
|
||||
const pendingPetsPromise = petStoreApi.pet.findPetsByStatus({ status: 'pending' })
|
||||
const soldPetsPromise = petStoreApi.pet.findPetsByStatus({ status: 'sold' })
|
||||
const availablePetsPromise = petStoreApi.pet.findPetsByStatus({
|
||||
status: StatusEnum.Available,
|
||||
})
|
||||
const pendingPetsPromise = petStoreApi.pet.findPetsByStatus({
|
||||
status: StatusEnum.Pending,
|
||||
})
|
||||
const soldPetsPromise = petStoreApi.pet.findPetsByStatus({
|
||||
status: StatusEnum.Sold,
|
||||
})
|
||||
|
||||
const [availablePets, pendingPets, soldPets] = await Promise.all([
|
||||
availablePetsPromise,
|
||||
@@ -45,9 +51,15 @@ export default async function PetsDashboardPage() {
|
||||
|
||||
```tsx
|
||||
export default async function PetsDashboardPage() {
|
||||
const availablePets = await petStoreApi.pet.findPetsByStatus({ status: 'available' })
|
||||
const pendingPets = await petStoreApi.pet.findPetsByStatus({ status: 'pending' })
|
||||
const soldPets = await petStoreApi.pet.findPetsByStatus({ status: 'sold' })
|
||||
const availablePets = await petStoreApi.pet.findPetsByStatus({
|
||||
status: StatusEnum.Available,
|
||||
})
|
||||
const pendingPets = await petStoreApi.pet.findPetsByStatus({
|
||||
status: StatusEnum.Pending,
|
||||
})
|
||||
const soldPets = await petStoreApi.pet.findPetsByStatus({
|
||||
status: StatusEnum.Sold,
|
||||
})
|
||||
|
||||
return (
|
||||
<PetsDashboardScreen
|
||||
@@ -68,8 +80,8 @@ export default async function PetsDashboardPage() {
|
||||
```tsx
|
||||
export default async function OrderPage({ params }: OrderPageProps) {
|
||||
const { id } = await params
|
||||
const order = await petStoreApi.store.getOrderById(Number(id))
|
||||
const pet = await petStoreApi.pet.getPetById(order.petId)
|
||||
const order = await petStoreApi.store.getOrderById({ orderId: Number(id) })
|
||||
const pet = await petStoreApi.pet.getPetById({ petId: order.petId })
|
||||
|
||||
return <OrderScreen order={order} pet={pet} />
|
||||
}
|
||||
@@ -19,13 +19,15 @@ keywords: [rest, promise, suspense, streaming, server components]
|
||||
```tsx
|
||||
// src/app/(routes)/pets/page.tsx
|
||||
import { Suspense } from 'react'
|
||||
import { petStoreApi } from 'infra/pet-store-api'
|
||||
import { petStoreApi, StatusEnum } from 'infra/pet-store-api'
|
||||
import { PetListSection } from 'widgets/pet-list-section'
|
||||
import { PetListSkeleton } from 'widgets/pet-list-section'
|
||||
import type { Pet } from 'infra/pet-store-api'
|
||||
|
||||
export default function PetsPage() {
|
||||
const petsPromise = petStoreApi.pet.findPetsByStatus({ status: 'available' })
|
||||
const petsPromise = petStoreApi.pet.findPetsByStatus({
|
||||
status: StatusEnum.Available,
|
||||
})
|
||||
|
||||
return (
|
||||
<main>
|
||||
@@ -29,12 +29,12 @@ SSR/dynamic rendering нужен, когда данные зависят от т
|
||||
|
||||
```tsx
|
||||
// src/app/(routes)/pets/page.tsx
|
||||
import { petStoreApi } from 'infra/pet-store-api'
|
||||
import { petStoreApi, StatusEnum } from 'infra/pet-store-api'
|
||||
import { PetsScreen } from 'screens/pets'
|
||||
|
||||
export default async function PetsPage() {
|
||||
const pets = await petStoreApi.pet.findPetsByStatus({
|
||||
status: 'available',
|
||||
status: StatusEnum.Available,
|
||||
})
|
||||
|
||||
return <PetsScreen pets={pets} />
|
||||
@@ -57,7 +57,7 @@ type PetPageProps = {
|
||||
|
||||
export default async function PetPage({ params }: PetPageProps) {
|
||||
const { id } = await params
|
||||
const pet = await petStoreApi.pet.getPetById(Number(id)).catch(() => null)
|
||||
const pet = await petStoreApi.pet.getPetById({ petId: Number(id) }).catch(() => null)
|
||||
|
||||
if (!pet) {
|
||||
notFound()
|
||||
@@ -73,7 +73,7 @@ export default async function PetPage({ params }: PetPageProps) {
|
||||
|
||||
```tsx
|
||||
// Плохо — хуки нельзя вызывать в Server Component
|
||||
const { data } = useGetPetList('available')
|
||||
const { data } = useGetPetList({ status: StatusEnum.Available })
|
||||
|
||||
// Плохо — прямой fetch в обход клиента
|
||||
const response = await fetch('https://petstore3.swagger.io/api/v3/pet/findByStatus')
|
||||
@@ -134,7 +134,7 @@ export default async function FeedLayout({ children }: FeedLayoutProps) {
|
||||
}
|
||||
```
|
||||
|
||||
Подробнее о стратегиях запросов и начальных данных для клиентских хуков: [REST → Стратегии получения данных](../data/rest/strategies/index.md), [REST → Начальные данные для клиентских хуков](../data/rest/strategies/client-hooks-initial-data.md).
|
||||
Подробнее о стратегиях запросов и начальных данных для клиентских хуков: [Получение данных](./data-fetch/index.md), [Начальные данные для клиентских хуков](./data-fetch/client-hooks-initial-data.md).
|
||||
|
||||
## Инициализация состояния
|
||||
|
||||
|
||||
50
ai/nextjs-style-guide/applied/rest-client/index.md
Normal file
50
ai/nextjs-style-guide/applied/rest-client/index.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: REST-клиент
|
||||
description: Настройка REST-клиента сервиса для работы с внешним API.
|
||||
keywords: [rest, api, данные, infra, клиент, swr, стратегии]
|
||||
---
|
||||
|
||||
# REST-клиент
|
||||
|
||||
Настройка REST-клиента сервиса для работы с внешним API.
|
||||
|
||||
## Настройка
|
||||
|
||||
Для каждого внешнего сервиса создаётся отдельный API-клиент: `pet-store-api`, `billing-api`, `maps-api`.
|
||||
|
||||
На этом этапе внешний API оформляется как модуль слоя `infra/`.
|
||||
|
||||
Клиент отвечает за:
|
||||
|
||||
- генерацию или ручное описание методов API;
|
||||
- настройку `baseUrl`;
|
||||
- заголовки и авторизацию;
|
||||
- обработку ошибок;
|
||||
- кастомизацию и расширение типов;
|
||||
- GET-хуки для клиентских компонентов;
|
||||
- прямое использование методов клиента в серверном коде и submit-функциях;
|
||||
- публичный API модуля.
|
||||
|
||||
Если у API есть OpenAPI-спецификация — клиент генерируется автоматически. Если OpenAPI нет или он неполный — клиент создаётся вручную.
|
||||
|
||||
GET-хуки относятся к клиенту, потому что это прозрачные SWR-обёртки над GET-методами этого клиента.
|
||||
|
||||
Подробнее:
|
||||
|
||||
- [Настройка REST-клиента](./setup/index.md)
|
||||
- [Автогенерация из OpenAPI](./setup/auto.md)
|
||||
- [Ручное создание](./setup/manual.md)
|
||||
- [GET-хуки REST-клиента](./setup/hooks.md)
|
||||
- [Использование REST-клиента](./usage.md)
|
||||
|
||||
## Как читать раздел
|
||||
|
||||
Если API ещё не подключён — начните с [Настройки REST-клиента](./setup/index.md).
|
||||
|
||||
Если клиент уже создан и нужно вызвать его методы — откройте [Использование REST-клиента](./usage.md).
|
||||
|
||||
Если клиент уже есть, но непонятно как получить данные — начните с раздела [Получение данных](../data-fetch/index.md).
|
||||
|
||||
Если данные нужны в Client Component — сначала проверьте, есть ли [GET-хук REST-клиента](./setup/hooks.md).
|
||||
|
||||
Если в коде появляется бизнес-смысл вроде `isAuth`, `canEdit`, `hasAccess` — это уже не REST-клиент, а `business/`.
|
||||
@@ -1,10 +1,14 @@
|
||||
---
|
||||
title: Автогенерация из OpenAPI
|
||||
description: Генерация REST-клиента из OpenAPI-спецификации через @gromlab/api-codegen.
|
||||
title: Автогенерация REST-клиента
|
||||
description: Генерация REST-клиента из OpenAPI-спецификации.
|
||||
keywords: [rest, openapi, api-codegen, автогенерация, generated, npx]
|
||||
---
|
||||
|
||||
# Автогенерация из OpenAPI
|
||||
# Автогенерация REST-клиента
|
||||
|
||||
Генерация REST-клиента из OpenAPI-спецификации.
|
||||
|
||||
## Когда использовать
|
||||
|
||||
Автогенерация используется, когда у API есть актуальная OpenAPI-спецификация. Генератор создаёт TypeScript-клиент, типы и методы API, а разработчик вручную добавляет настройку клиента и GET-хуки.
|
||||
|
||||
@@ -66,12 +70,39 @@ src/infra/pet-store-api/generated/
|
||||
Для Petstore нужны GET-операции вида:
|
||||
|
||||
```ts
|
||||
petStoreApi.pet.findPetsByStatus(...)
|
||||
petStoreApi.pet.getPetById(...)
|
||||
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`
|
||||
|
||||
Сгенерированный код не должен напрямую использоваться из приложения. Сначала создаётся настроенный инстанс клиента.
|
||||
@@ -80,8 +111,14 @@ petStoreApi.pet.getPetById(...)
|
||||
// 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: 'https://petstore3.swagger.io/api/v3',
|
||||
baseUrl,
|
||||
baseApiParams: {
|
||||
secure: false,
|
||||
headers: {
|
||||
@@ -93,10 +130,47 @@ const httpClient = new HttpClient({
|
||||
export const petStoreApi = new Api(httpClient)
|
||||
```
|
||||
|
||||
В реальном проекте вместо строки `baseUrl` обычно берётся из env-переменной, нормализуется в `client.ts` и передаётся в `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/`.
|
||||
@@ -160,7 +234,12 @@ export type { TermRecordItemExtended } from './term'
|
||||
```ts
|
||||
// src/infra/pet-store-api/index.ts
|
||||
export { petStoreApi } from './client'
|
||||
export type { Pet } from './generated/pet-store-api.generated'
|
||||
export type {
|
||||
FindPetsByStatusParams,
|
||||
GetPetByIdParams,
|
||||
Pet,
|
||||
} from './generated/pet-store-api.generated'
|
||||
export { PetStatusEnum, StatusEnum } from './generated/pet-store-api.generated'
|
||||
export * from './hooks'
|
||||
```
|
||||
|
||||
@@ -190,4 +269,4 @@ npm run codegen:pet-store-api
|
||||
|
||||
## Следующий шаг
|
||||
|
||||
После генерации и настройки `client.ts` проверьте серверный вызов метода клиента или добавьте [GET-хук REST-клиента](./hooks.md) для Client Components.
|
||||
После генерации и настройки `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).
|
||||
@@ -1,14 +1,18 @@
|
||||
---
|
||||
title: Создание клиента
|
||||
description: Из чего состоит REST-клиент и какие части нужно подготовить перед использованием API.
|
||||
title: Настройка REST-клиента
|
||||
description: Подготовка REST-клиента сервиса к использованию.
|
||||
keywords: [rest, клиент, infra, методы, openapi, get-хуки, swr]
|
||||
---
|
||||
|
||||
# Создание клиента
|
||||
# Настройка REST-клиента
|
||||
|
||||
Подготовка REST-клиента сервиса к использованию.
|
||||
|
||||
## Что настраиваем
|
||||
|
||||
REST-клиент — это infra-модуль, через который проект работает с внешним REST API.
|
||||
|
||||
На этом этапе нужно подготовить клиент сервиса: создать оболочку клиента, получить методы API и добавить GET-хуки для клиентских компонентов.
|
||||
На этапе настройки нужно подготовить клиент сервиса: оболочку клиента, методы API и GET-хуки для клиентских компонентов.
|
||||
|
||||
## Из чего состоит клиент
|
||||
|
||||
@@ -28,6 +32,8 @@ REST-клиент состоит из трёх основных частей:
|
||||
|
||||
`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.
|
||||
@@ -50,6 +56,10 @@ REST-клиент состоит из трёх основных частей:
|
||||
|
||||
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)
|
||||
@@ -61,15 +71,18 @@ src/infra/{service-name}/
|
||||
├── client.ts # самописная оболочка и инстанс клиента
|
||||
├── generated/ или methods/ # методы API
|
||||
├── hooks/ # GET-хуки REST-клиента
|
||||
├── types/ # DTO, типы API и расширения типов
|
||||
├── 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. После создания клиента переходите к [Стратегиям получения данных](../strategies/index.md).
|
||||
3. Проверьте прямые вызовы клиента: [Использование REST-клиента](../usage.md).
|
||||
4. После настройки клиента переходите к [Получению данных](../../data-fetch/index.md).
|
||||
@@ -1,10 +1,14 @@
|
||||
---
|
||||
title: Ручное создание
|
||||
title: Ручное создание REST-клиента
|
||||
description: Создание REST-клиента вручную, когда OpenAPI нет или он неполный.
|
||||
keywords: [rest, ручной клиент, fetch, methods, dto, errors, infra]
|
||||
---
|
||||
|
||||
# Ручное создание
|
||||
# Ручное создание REST-клиента
|
||||
|
||||
Создание REST-клиента вручную, когда OpenAPI нет или он неполный.
|
||||
|
||||
## Когда использовать
|
||||
|
||||
Ручной REST-клиент используется, когда у API нет OpenAPI-спецификации или она недостаточно точная для автогенерации.
|
||||
|
||||
@@ -159,8 +163,14 @@ export function postsMethods(client: PetProjectApiClient) {
|
||||
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(
|
||||
process.env.NEXT_PUBLIC_API_URL ?? '',
|
||||
baseUrl,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
)
|
||||
|
||||
@@ -180,8 +190,9 @@ export * from './hooks'
|
||||
- `fetch` используется только внутри базового клиента.
|
||||
- DTO запросов и ответов живут в `types/`.
|
||||
- `client.ts` не содержит DTO, GET-хуки и бизнес-логику.
|
||||
- `baseUrl` берётся из обязательной env-переменной без fallback-значения в коде.
|
||||
- Методы лежат в `methods/` и возвращают DTO.
|
||||
- GET-хуки добавляются отдельно в `hooks/`, если данные нужны в Client Components.
|
||||
- Доменные типы и маппинг DTO живут не в REST-клиенте, а в `business/`.
|
||||
|
||||
Следующий шаг: [GET-хуки REST-клиента](./hooks.md) или [Стратегии получения данных](../strategies/index.md).
|
||||
Следующий шаг: [Использование REST-клиента](../usage.md), [GET-хуки REST-клиента](./hooks.md) или [Получение данных](../../data-fetch/index.md).
|
||||
21
ai/nextjs-style-guide/applied/rest-client/usage.md
Normal file
21
ai/nextjs-style-guide/applied/rest-client/usage.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: Использование REST-клиента
|
||||
description: Как вызвать готовый REST-клиент в функции.
|
||||
keywords: [rest, api client, submit, generated, pet-store-api]
|
||||
---
|
||||
|
||||
# Использование REST-клиента
|
||||
|
||||
Как вызвать готовый REST-клиент в функции.
|
||||
|
||||
## Пример
|
||||
|
||||
```ts
|
||||
import { petStoreApi } from 'infra/pet-store-api'
|
||||
|
||||
export const getPet = async (petId: number) => {
|
||||
const pet = await petStoreApi.pet.getPetById({ petId })
|
||||
|
||||
console.log(pet)
|
||||
}
|
||||
```
|
||||
@@ -1,60 +0,0 @@
|
||||
---
|
||||
title: Источники данных
|
||||
description: Какие источники данных используются в проекте и как с ними работать.
|
||||
keywords: [данные, api, rest, realtime, клиент, swr, infra, введение, карта раздела]
|
||||
---
|
||||
|
||||
# Источники данных
|
||||
|
||||
Какие источники данных используются в проекте и как с ними работать.
|
||||
|
||||
## Принципы раздела
|
||||
|
||||
- **Клиент — в `infra/`.** Каждый внешний сервис — отдельный модуль слоя `infra/{service-name}/`.
|
||||
- **Прямой `fetch` запрещён.** Запросы идут только через клиент модуля. Исключения — точечные и обоснованные.
|
||||
- **Источник данных диктует канал.** REST, realtime и т.п. — независимые подразделы, у каждого своя модель клиента и своё потребление.
|
||||
- **Серверные и клиентские компоненты потребляют по-разному.** Server Components — прямой `await` метода клиента, клиентские — через готовые GET-хуки REST-клиента (`useGetUserList`, `useGetPostDetail` и т.п.). SWR инкапсулирован в хуке, компонент про него не знает.
|
||||
|
||||
## Карта раздела
|
||||
|
||||
### REST
|
||||
|
||||
Канал «запрос-ответ» по HTTP. Покрывает большинство API.
|
||||
|
||||
- [REST](./rest/index.md) — обзор раздела: создание клиента и использование.
|
||||
- **Создание клиента** — как оформляется REST API в проекте:
|
||||
- [Обзор](./rest/clients/index.md) — когда нужен клиент и как выбрать подход.
|
||||
- [Автогенерация из OpenAPI](./rest/clients/auto.md) — для API с OpenAPI-спецификацией, через `@gromlab/api-codegen`.
|
||||
- [Ручное создание](./rest/clients/manual.md) — для API без схемы, клиент пишется и поддерживается руками.
|
||||
- [GET-хуки REST-клиента](./rest/clients/hooks.md) — прозрачные SWR-обёртки над GET-методами клиента.
|
||||
- **Использование** — как получать данные через готовый клиент:
|
||||
- [Стратегии получения данных](./rest/strategies/index.md) — как выбрать способ получения данных под ситуацию.
|
||||
- [Серверный await](./rest/strategies/server-await.md) — прямой `await` метода клиента в Server Components.
|
||||
- [Параллельные серверные запросы](./rest/strategies/parallel-server-requests.md) — запуск независимых серверных запросов без waterfall.
|
||||
- [Передача промиса ниже](./rest/strategies/pass-promise-down.md) — серверный стриминг через промис и `Suspense`.
|
||||
- [Начальные данные для клиентских хуков](./rest/strategies/client-hooks-initial-data.md) — серверный промис в `SWRConfig fallback`.
|
||||
- [Клиентский GET-хук](./rest/strategies/client-get-hook.md) — получение данных в Client Components через готовый GET-хук.
|
||||
- [Business-композиция](./rest/strategies/business-composition.md) — доменная интерпретация и композиция REST-данных.
|
||||
|
||||
### Realtime
|
||||
|
||||
Канал push-данных: WebSocket, SSE, событийные шины. Транспорт не зашит в правила — важна абстракция «подписка».
|
||||
|
||||
- [Realtime](./realtime.md) — клиент realtime в `infra/`, потребление через `useSWRSubscription` или прямые подписки.
|
||||
|
||||
## Что даёт раздел
|
||||
|
||||
После прочтения раздела понятно:
|
||||
|
||||
- Где живёт код работы с API и почему именно там.
|
||||
- Когда генерировать клиент автоматически, а когда писать вручную, и как структурирован каждый из вариантов.
|
||||
- Какие GET-хуки относятся к REST-клиенту и почему они живут в `infra/{service-name}/hooks/`.
|
||||
- Как выбрать стратегию получения REST-данных под конкретную ситуацию.
|
||||
- Как подключать realtime-источники в общую модель работы с данными.
|
||||
- Какие правила обязательны и какие отклонения допустимы.
|
||||
|
||||
## Что не входит в раздел
|
||||
|
||||
- **Глобальное состояние UI** — Stores, формы, фичефлаги. Это [Stores](../applied/stores.md).
|
||||
- **Доменная логика** — как данные превращаются в сценарии бизнеса. Это слой `business/` в [Архитектуре](../basics/architecture/index.md).
|
||||
- **Хуки общего назначения** — переиспользуемые хуки UI, не привязанные к конкретному API. Отдельный прикладной раздел для них пока не ведётся.
|
||||
@@ -1,79 +0,0 @@
|
||||
---
|
||||
title: Realtime
|
||||
description: "Работа с push-данными от сервера: подписки и события."
|
||||
keywords: [realtime, websocket, sse, подписка, swr subscription, useSWRSubscription, push, события]
|
||||
---
|
||||
|
||||
# Realtime
|
||||
|
||||
Работа с push-данными от сервера: подписки и события.
|
||||
|
||||
## Принципы
|
||||
|
||||
- **Клиент realtime — в `infra/`** отдельным модулем по имени канала. То же правило, что и для REST: никаких прямых соединений в коде приложения.
|
||||
- **Подписка — единица потребления.** Клиент даёт функцию `subscribe(topic, handler) → unsubscribe`. Внутри — конкретный транспорт.
|
||||
- **Использование на клиенте — два сценария:**
|
||||
- **`useSWRSubscription`** — для данных, которые показываются в UI и должны кешироваться/синхронизироваться с REST.
|
||||
- **Прямая подписка** — для побочных эффектов (тосты, нотификации, аналитика), не привязанных к рендеру.
|
||||
|
||||
## Размещение клиента
|
||||
|
||||
```text
|
||||
src/infra/
|
||||
└── {channel-name}/
|
||||
├── connection.ts # установление соединения, реконнект
|
||||
├── subscribe.ts # subscribe(topic, handler) → unsubscribe
|
||||
├── types.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## Использование через SWR
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import useSWRSubscription from 'swr/subscription'
|
||||
import { subscribe } from 'infra/notifications'
|
||||
|
||||
export function NotificationCounter() {
|
||||
const { data: count } = useSWRSubscription(
|
||||
['notifications', 'count'],
|
||||
(key, { next }) =>
|
||||
subscribe('notifications.count', (value: number) => next(null, value)),
|
||||
)
|
||||
|
||||
return <span>{count ?? 0}</span>
|
||||
}
|
||||
```
|
||||
|
||||
Плюсы: кеш и дедупликация подписки между несколькими местами рендера; единая модель данных с REST.
|
||||
|
||||
## Прямая подписка
|
||||
|
||||
Для побочных эффектов, которые не влияют на состояние UI напрямую:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { subscribe } from 'infra/notifications'
|
||||
import { showToast } from 'ui/toast'
|
||||
|
||||
export function NotificationsToaster() {
|
||||
useEffect(() => {
|
||||
return subscribe('notifications.new', (notification) => {
|
||||
showToast(notification.message)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
Возврат `unsubscribe` из `useEffect` обязателен — иначе утечка подписки.
|
||||
|
||||
## Запрет прямых соединений
|
||||
|
||||
Создавать `new WebSocket(...)`, `new EventSource(...)` или подписываться на событийные шины напрямую в коде приложения — запрещено. Все соединения проходят через клиент в `infra/`.
|
||||
|
||||
Исключения — точечные и обоснованные (например, диагностический скрипт), помечаются комментарием.
|
||||
@@ -1,206 +0,0 @@
|
||||
---
|
||||
title: GET-хуки REST-клиента
|
||||
description: Прозрачные SWR-обёртки над GET-методами REST-клиента.
|
||||
keywords: [rest, swr, get-хуки, client components, infra]
|
||||
---
|
||||
|
||||
# GET-хуки REST-клиента
|
||||
|
||||
GET-хуки REST-клиента — прозрачные SWR-обёртки над GET-методами API-клиента. Они нужны, чтобы Client Components получали данные с кешированием, дедупликацией и ревалидацией, не работая с `useSWR` напрямую.
|
||||
|
||||
## Где лежат
|
||||
|
||||
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
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## Контракт
|
||||
|
||||
- Один GET-хук = один GET-метод клиента.
|
||||
- Имя GET-хука начинается с `useGet`: `useGetPetList`, `useGetPetDetail`.
|
||||
- Имя файла начинается с `use-get`: `use-get-pet-list.hook.ts`.
|
||||
- Хук принимает только параметры GET-метода и `config?: SWRConfiguration`.
|
||||
- Что передали хуку, то он передаёт в GET-метод.
|
||||
- Внутри только SWR-механика: key, fetcher, `useSWR`, `config`.
|
||||
- Хук возвращает тип ответа API: generated-тип или DTO из `types/`.
|
||||
- Хук не объединяет несколько запросов.
|
||||
- Хук не маппит DTO в доменную модель.
|
||||
- Хук не вычисляет бизнес-флаги: `isAuth`, `canEdit`, `hasAccess`, `hasPets`.
|
||||
- Хук не вызывает тосты, модалки, редиректы и не пишет UI-состояние.
|
||||
|
||||
## Пример списка
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-api/hooks/use-get-pet-list.hook.ts
|
||||
import useSWR from 'swr'
|
||||
import type { SWRConfiguration } from 'swr'
|
||||
import { petStoreApi } from '../client'
|
||||
import type { Pet } from '../generated/pet-store-api.generated'
|
||||
|
||||
export type PetStatus = 'available' | 'pending' | 'sold'
|
||||
|
||||
export const getPetListKey = (status: PetStatus) =>
|
||||
['pet-store-api', 'pet', 'list', status] as const
|
||||
|
||||
/**
|
||||
* Получение списка питомцев по статусу.
|
||||
*/
|
||||
export const useGetPetList = (status: PetStatus | null, config?: SWRConfiguration) => {
|
||||
const isReady = status !== null
|
||||
const key = isReady ? getPetListKey(status) : null
|
||||
const fetcher = () => petStoreApi.pet.findPetsByStatus({ status })
|
||||
|
||||
return useSWR<Pet[]>(key, fetcher, config)
|
||||
}
|
||||
```
|
||||
|
||||
Функция `getPetListKey` нужна, чтобы один и тот же SWR-ключ использовался внутри GET-хука и при передаче начальных данных через `SWRConfig fallback`.
|
||||
|
||||
Пример начальных данных для клиентского хука:
|
||||
|
||||
```tsx
|
||||
import type { ReactNode } from 'react'
|
||||
import { SWRConfig, unstable_serialize } from 'swr'
|
||||
import {
|
||||
getPetListKey,
|
||||
petStoreApi,
|
||||
} from 'infra/pet-store-api'
|
||||
|
||||
export default function PetsLayout({ children }: { children: ReactNode }) {
|
||||
const petsPromise = petStoreApi.pet.findPetsByStatus({ status: 'available' })
|
||||
|
||||
return (
|
||||
<SWRConfig
|
||||
value={{
|
||||
fallback: {
|
||||
[unstable_serialize(getPetListKey('available'))]: petsPromise,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SWRConfig>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Клиентский компонент при этом ничего не знает про preload/fallback и продолжает вызывать обычный хук:
|
||||
|
||||
```tsx
|
||||
const { data: pets } = useGetPetList('available')
|
||||
```
|
||||
|
||||
## Пример detail-запроса
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-api/hooks/use-get-pet-detail.hook.ts
|
||||
import useSWR from 'swr'
|
||||
import type { SWRConfiguration } from 'swr'
|
||||
import { petStoreApi } from '../client'
|
||||
import type { Pet } from '../generated/pet-store-api.generated'
|
||||
|
||||
export const getPetDetailKey = (id: number) =>
|
||||
['pet-store-api', 'pet', 'detail', id] as const
|
||||
|
||||
/**
|
||||
* Получение питомца по идентификатору.
|
||||
*/
|
||||
export const useGetPetDetail = (id: number | null, config?: SWRConfiguration) => {
|
||||
const isReady = id !== null
|
||||
const key = isReady ? getPetDetailKey(id) : null
|
||||
const fetcher = () => petStoreApi.pet.getPetById(id)
|
||||
|
||||
return useSWR<Pet>(key, fetcher, config)
|
||||
}
|
||||
```
|
||||
|
||||
## Отложенный запрос через `null`
|
||||
|
||||
GET-хук может принимать `null` для обязательного параметра. `null` означает, что параметр ещё не готов и запрос выполнять нельзя.
|
||||
|
||||
Внутри хука это выражается через `isReady`: если параметр не готов, ключ SWR становится `null`, и SWR не вызывает fetcher.
|
||||
|
||||
```ts
|
||||
const isReady = id !== null
|
||||
const key = isReady ? getPetDetailKey(id) : null
|
||||
```
|
||||
|
||||
`null` не передаётся в метод клиента. Key-функция принимает только готовые параметры, поэтому её можно безопасно использовать для начальных данных через `SWRConfig fallback`.
|
||||
|
||||
Для числовых идентификаторов не используйте проверку `if (id)`: значение `0` тоже валидное число. Проверяйте явно: `id !== null`.
|
||||
|
||||
## Экспорт
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-api/hooks/index.ts
|
||||
export { getPetListKey, useGetPetList } from './use-get-pet-list.hook'
|
||||
export type { PetStatus } from './use-get-pet-list.hook'
|
||||
export { getPetDetailKey, useGetPetDetail } from './use-get-pet-detail.hook'
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-api/index.ts
|
||||
export { petStoreApi } from './client'
|
||||
export type { Pet } from './generated/pet-store-api.generated'
|
||||
export * from './hooks'
|
||||
```
|
||||
|
||||
## Где заканчивается infra
|
||||
|
||||
```ts
|
||||
// Хорошо: infra, прозрачный GET-хук
|
||||
const { data: pets } = useGetPetList('available')
|
||||
```
|
||||
|
||||
```ts
|
||||
// Хорошо: business, доменная интерпретация
|
||||
export const useAvailablePets = () => {
|
||||
const query = useGetPetList('available')
|
||||
|
||||
return {
|
||||
...query,
|
||||
hasPets: Boolean(query.data?.length),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`hasPets` — не часть GET-запроса, поэтому он не добавляется в `useGetPetList`.
|
||||
|
||||
## Что запрещено
|
||||
|
||||
```ts
|
||||
// Плохо — useSWR в компоненте
|
||||
const { data } = useSWR(
|
||||
['pet-store-api', 'pet', 'list', status],
|
||||
() => petStoreApi.pet.findPetsByStatus({ status }),
|
||||
)
|
||||
|
||||
// Плохо — несколько GET внутри infra-хука
|
||||
export const usePetDashboard = () => {
|
||||
const available = useGetPetList('available')
|
||||
const sold = useGetPetList('sold')
|
||||
|
||||
return { available, sold }
|
||||
}
|
||||
|
||||
// Плохо — бизнес-флаг внутри GET-хука REST-клиента
|
||||
export const useGetPetList = (status: PetStatus) => {
|
||||
const query = useSWR(...)
|
||||
|
||||
return {
|
||||
...query,
|
||||
hasPets: Boolean(query.data?.length),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Подробное потребление таких хуков описано в стратегии [Клиентский GET-хук](../strategies/client-get-hook.md).
|
||||
@@ -1,74 +0,0 @@
|
||||
---
|
||||
title: REST
|
||||
description: Как правильно работать с REST API в проекте.
|
||||
keywords: [rest, api, данные, infra, клиент, swr, стратегии]
|
||||
---
|
||||
|
||||
# REST
|
||||
|
||||
Раздел описывает, как правильно работать с REST API в проекте: создать клиент сервиса и выбрать способ получения данных в приложении.
|
||||
|
||||
REST в проекте проходит через два главных этапа:
|
||||
|
||||
1. Создание клиента.
|
||||
2. Использование.
|
||||
|
||||
## 1. Создание клиента
|
||||
|
||||
На этом этапе внешний API оформляется как модуль слоя `infra/`.
|
||||
|
||||
Клиент отвечает за:
|
||||
|
||||
- генерацию или ручное описание методов API;
|
||||
- настройку `baseUrl`;
|
||||
- заголовки и авторизацию;
|
||||
- обработку ошибок;
|
||||
- кастомизацию и расширение типов;
|
||||
- GET-хуки для клиентских компонентов;
|
||||
- публичный API модуля.
|
||||
|
||||
Если у API есть OpenAPI-спецификация — клиент генерируется автоматически. Если OpenAPI нет или он неполный — клиент создаётся вручную.
|
||||
|
||||
GET-хуки относятся к клиенту, потому что это прозрачные SWR-обёртки над GET-методами этого клиента.
|
||||
|
||||
Подробнее:
|
||||
|
||||
- [Создание клиента](./clients/index.md)
|
||||
- [Автогенерация из OpenAPI](./clients/auto.md)
|
||||
- [Ручное создание](./clients/manual.md)
|
||||
- [GET-хуки REST-клиента](./clients/hooks.md)
|
||||
|
||||
## 2. Использование
|
||||
|
||||
После создания клиента нужно определить рендер страницы и выбрать, как получать данные в конкретном месте приложения.
|
||||
|
||||
Раздел использования отвечает на вопросы:
|
||||
|
||||
- как понять, можно ли сохранить static/ISR;
|
||||
- когда страница становится dynamic/SSR;
|
||||
- когда получать данные через серверный `await`;
|
||||
- когда запускать несколько серверных запросов параллельно;
|
||||
- когда передавать промис ниже по дереву;
|
||||
- когда передавать начальные данные клиентским GET-хукам;
|
||||
- когда использовать GET-хук в клиентском компоненте;
|
||||
- когда выносить композицию и бизнес-смысл в `business/`.
|
||||
|
||||
Подробнее:
|
||||
|
||||
- [Стратегии получения данных](./strategies/index.md)
|
||||
- [Серверный await](./strategies/server-await.md)
|
||||
- [Параллельные серверные запросы](./strategies/parallel-server-requests.md)
|
||||
- [Передача промиса ниже](./strategies/pass-promise-down.md)
|
||||
- [Начальные данные для клиентских хуков](./strategies/client-hooks-initial-data.md)
|
||||
- [Клиентский GET-хук](./strategies/client-get-hook.md)
|
||||
- [Business-композиция](./strategies/business-composition.md)
|
||||
|
||||
## Как читать раздел
|
||||
|
||||
Если API ещё не подключён — начните с [Создания клиента](./clients/index.md).
|
||||
|
||||
Если клиент уже есть, но непонятно как получить данные — начните со [Стратегий получения данных](./strategies/index.md).
|
||||
|
||||
Если данные нужны в Client Component — сначала проверьте, есть ли [GET-хук REST-клиента](./clients/hooks.md).
|
||||
|
||||
Если в коде появляется бизнес-смысл вроде `isAuth`, `canEdit`, `hasAccess` — это уже не REST-клиент, а `business/`.
|
||||
Reference in New Issue
Block a user