From 867ea244cf7afaece4c0ef9cb4e7e3be356d0273 Mon Sep 17 00:00:00 2001 From: "S.Gromov" Date: Fri, 8 May 2026 19:34:39 +0300 Subject: [PATCH] =?UTF-8?q?style:=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=B4=20=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai/nextjs-style-guide/MAP.md | 39 +-- ai/nextjs-style-guide/VERSION | 4 +- ai/nextjs-style-guide/applied/biome.md | 63 +++- .../creating-project/from-template.md | 4 +- .../{ => applied}/creating-project/manual.md | 30 +- .../{ => applied}/creating-project/nextjs.md | 14 +- .../data-fetch}/business-composition.md | 14 +- .../data-fetch}/client-get-hook.md | 11 +- .../data-fetch}/client-hooks-initial-data.md | 24 +- .../data-fetch}/index.md | 14 +- .../data-fetch}/parallel-server-requests.md | 30 +- .../data-fetch}/pass-promise-down.md | 6 +- .../data-fetch}/server-await.md | 8 +- ai/nextjs-style-guide/applied/page-level.md | 2 +- .../applied/rest-client/index.md | 50 +++ .../rest-client/setup}/auto.md | 97 +++++- .../applied/rest-client/setup/hooks.md | 313 ++++++++++++++++++ .../rest-client/setup}/index.md | 25 +- .../rest-client/setup}/manual.md | 19 +- .../applied/rest-client/usage.md | 21 ++ ai/nextjs-style-guide/data/index.md | 60 ---- ai/nextjs-style-guide/data/realtime.md | 79 ----- .../data/rest/clients/hooks.md | 206 ------------ ai/nextjs-style-guide/data/rest/index.md | 74 ----- 24 files changed, 661 insertions(+), 546 deletions(-) rename ai/nextjs-style-guide/{ => applied}/creating-project/from-template.md (98%) rename ai/nextjs-style-guide/{ => applied}/creating-project/manual.md (85%) rename ai/nextjs-style-guide/{ => applied}/creating-project/nextjs.md (92%) rename ai/nextjs-style-guide/{data/rest/strategies => applied/data-fetch}/business-composition.md (88%) rename ai/nextjs-style-guide/{data/rest/strategies => applied/data-fetch}/client-get-hook.md (89%) rename ai/nextjs-style-guide/{data/rest/strategies => applied/data-fetch}/client-hooks-initial-data.md (87%) rename ai/nextjs-style-guide/{data/rest/strategies => applied/data-fetch}/index.md (91%) rename ai/nextjs-style-guide/{data/rest/strategies => applied/data-fetch}/parallel-server-requests.md (79%) rename ai/nextjs-style-guide/{data/rest/strategies => applied/data-fetch}/pass-promise-down.md (94%) rename ai/nextjs-style-guide/{data/rest/strategies => applied/data-fetch}/server-await.md (94%) create mode 100644 ai/nextjs-style-guide/applied/rest-client/index.md rename ai/nextjs-style-guide/{data/rest/clients => applied/rest-client/setup}/auto.md (57%) create mode 100644 ai/nextjs-style-guide/applied/rest-client/setup/hooks.md rename ai/nextjs-style-guide/{data/rest/clients => applied/rest-client/setup}/index.md (61%) rename ai/nextjs-style-guide/{data/rest/clients => applied/rest-client/setup}/manual.md (89%) create mode 100644 ai/nextjs-style-guide/applied/rest-client/usage.md delete mode 100644 ai/nextjs-style-guide/data/index.md delete mode 100644 ai/nextjs-style-guide/data/realtime.md delete mode 100644 ai/nextjs-style-guide/data/rest/clients/hooks.md delete mode 100644 ai/nextjs-style-guide/data/rest/index.md diff --git a/ai/nextjs-style-guide/MAP.md b/ai/nextjs-style-guide/MAP.md index f4d196a..eeafb89 100644 --- a/ai/nextjs-style-guide/MAP.md +++ b/ai/nextjs-style-guide/MAP.md @@ -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-спрайты и какие проблемы они решают. diff --git a/ai/nextjs-style-guide/VERSION b/ai/nextjs-style-guide/VERSION index 138d54c..1fbf5a5 100644 --- a/ai/nextjs-style-guide/VERSION +++ b/ai/nextjs-style-guide/VERSION @@ -1,2 +1,2 @@ -eadc462 -2026-05-08T04:14:35.127Z +8231356 +2026-05-08T16:14:00.876Z diff --git a/ai/nextjs-style-guide/applied/biome.md b/ai/nextjs-style-guide/applied/biome.md index b8a8483..9551821 100644 --- a/ai/nextjs-style-guide/applied/biome.md +++ b/ai/nextjs-style-guide/applied/biome.md @@ -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"], - "linter": { - "rules": { - "suspicious": { - "noUnknownAtRules": "off" - } - } + "$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 diff --git a/ai/nextjs-style-guide/creating-project/from-template.md b/ai/nextjs-style-guide/applied/creating-project/from-template.md similarity index 98% rename from ai/nextjs-style-guide/creating-project/from-template.md rename to ai/nextjs-style-guide/applied/creating-project/from-template.md index c549640..3a760e2 100644 --- a/ai/nextjs-style-guide/creating-project/from-template.md +++ b/ai/nextjs-style-guide/applied/creating-project/from-template.md @@ -14,11 +14,11 @@ keywords: [создать проект из шаблона, шаблон, templa - **Стек:** Next.js (App Router), TypeScript, React. - **Архитектура:** структура папок по SLM, алиасы импортов. -- **Качество кода:** Biome (линтер и форматер), настройки VS Code. +- **Качество кода:** Biome (линтер и форматтер), настройки VS Code. - **Стили:** PostCSS Modules с плагинами, токены, медиа-брейкпоинты. - **Ассеты:** генерация SVG-спрайтов. - **Кодогенерация:** шаблоны для страниц, компонентов, хуков, сторов. -в + ## Установка 1. Склонировать шаблон в родительском каталоге будущего проекта: diff --git a/ai/nextjs-style-guide/creating-project/manual.md b/ai/nextjs-style-guide/applied/creating-project/manual.md similarity index 85% rename from ai/nextjs-style-guide/creating-project/manual.md rename to ai/nextjs-style-guide/applied/creating-project/manual.md index d345a50..17d2835 100644 --- a/ai/nextjs-style-guide/creating-project/manual.md +++ b/ai/nextjs-style-guide/applied/creating-project/manual.md @@ -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-спрайты | Иконки через ``, управление цветом | [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-спрайты | Иконки через ``, управление цветом | [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-компонента ``. -См. [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). ## Правила diff --git a/ai/nextjs-style-guide/creating-project/nextjs.md b/ai/nextjs-style-guide/applied/creating-project/nextjs.md similarity index 92% rename from ai/nextjs-style-guide/creating-project/nextjs.md rename to ai/nextjs-style-guide/applied/creating-project/nextjs.md index 546f9f1..d22f331 100644 --- a/ai/nextjs-style-guide/creating-project/nextjs.md +++ b/ai/nextjs-style-guide/applied/creating-project/nextjs.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** — отдельные шаги установки, не в этом разделе. diff --git a/ai/nextjs-style-guide/data/rest/strategies/business-composition.md b/ai/nextjs-style-guide/applied/data-fetch/business-composition.md similarity index 88% rename from ai/nextjs-style-guide/data/rest/strategies/business-composition.md rename to ai/nextjs-style-guide/applied/data-fetch/business-composition.md index b9250e5..09202e8 100644 --- a/ai/nextjs-style-guide/data/rest/strategies/business-composition.md +++ b/ai/nextjs-style-guide/applied/data-fetch/business-composition.md @@ -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 { diff --git a/ai/nextjs-style-guide/data/rest/strategies/client-get-hook.md b/ai/nextjs-style-guide/applied/data-fetch/client-get-hook.md similarity index 89% rename from ai/nextjs-style-guide/data/rest/strategies/client-get-hook.md rename to ai/nextjs-style-guide/applied/data-fetch/client-get-hook.md index 6dde3e1..2525f2e 100644 --- a/ai/nextjs-style-guide/data/rest/strategies/client-get-hook.md +++ b/ai/nextjs-style-guide/applied/data-fetch/client-get-hook.md @@ -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('available') - const { data: pets, isLoading, error } = useGetPetList(status) + const [status, setStatus] = useState(StatusEnum.Available) + const { data: pets, isLoading, error } = useGetPetList({ status }) return (
@@ -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 }), ) ``` diff --git a/ai/nextjs-style-guide/data/rest/strategies/client-hooks-initial-data.md b/ai/nextjs-style-guide/applied/data-fetch/client-hooks-initial-data.md similarity index 87% rename from ai/nextjs-style-guide/data/rest/strategies/client-hooks-initial-data.md rename to ai/nextjs-style-guide/applied/data-fetch/client-hooks-initial-data.md index 610eb87..04845b3 100644 --- a/ai/nextjs-style-guide/data/rest/strategies/client-hooks-initial-data.md +++ b/ai/nextjs-style-guide/applied/data-fetch/client-hooks-initial-data.md @@ -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 ( @@ -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
Загрузка...
@@ -100,6 +107,7 @@ export function PetList() { ## Что важно - Ключ `fallback` должен совпадать с ключом GET-хука. +- `fallback` использует ту же key-функцию и те же params, что и GET-хук. - Серверный код вызывает метод клиента, а не GET-хук. - Клиентский компонент вызывает GET-хук, а не `useSWR` напрямую. - Эта стратегия не означает ручную работу с кешем в компонентах. diff --git a/ai/nextjs-style-guide/data/rest/strategies/index.md b/ai/nextjs-style-guide/applied/data-fetch/index.md similarity index 91% rename from ai/nextjs-style-guide/data/rest/strategies/index.md rename to ai/nextjs-style-guide/applied/data-fetch/index.md index a7e6d34..4d60c90 100644 --- a/ai/nextjs-style-guide/data/rest/strategies/index.md +++ b/ai/nextjs-style-guide/applied/data-fetch/index.md @@ -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-клиента diff --git a/ai/nextjs-style-guide/data/rest/strategies/parallel-server-requests.md b/ai/nextjs-style-guide/applied/data-fetch/parallel-server-requests.md similarity index 79% rename from ai/nextjs-style-guide/data/rest/strategies/parallel-server-requests.md rename to ai/nextjs-style-guide/applied/data-fetch/parallel-server-requests.md index fec2efb..d36beb0 100644 --- a/ai/nextjs-style-guide/data/rest/strategies/parallel-server-requests.md +++ b/ai/nextjs-style-guide/applied/data-fetch/parallel-server-requests.md @@ -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 ( } diff --git a/ai/nextjs-style-guide/data/rest/strategies/pass-promise-down.md b/ai/nextjs-style-guide/applied/data-fetch/pass-promise-down.md similarity index 94% rename from ai/nextjs-style-guide/data/rest/strategies/pass-promise-down.md rename to ai/nextjs-style-guide/applied/data-fetch/pass-promise-down.md index d2e5004..0c07820 100644 --- a/ai/nextjs-style-guide/data/rest/strategies/pass-promise-down.md +++ b/ai/nextjs-style-guide/applied/data-fetch/pass-promise-down.md @@ -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 (
diff --git a/ai/nextjs-style-guide/data/rest/strategies/server-await.md b/ai/nextjs-style-guide/applied/data-fetch/server-await.md similarity index 94% rename from ai/nextjs-style-guide/data/rest/strategies/server-await.md rename to ai/nextjs-style-guide/applied/data-fetch/server-await.md index 87c68b5..53f53e9 100644 --- a/ai/nextjs-style-guide/data/rest/strategies/server-await.md +++ b/ai/nextjs-style-guide/applied/data-fetch/server-await.md @@ -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 @@ -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') diff --git a/ai/nextjs-style-guide/applied/page-level.md b/ai/nextjs-style-guide/applied/page-level.md index 5a96eb6..3d41db0 100644 --- a/ai/nextjs-style-guide/applied/page-level.md +++ b/ai/nextjs-style-guide/applied/page-level.md @@ -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). ## Инициализация состояния diff --git a/ai/nextjs-style-guide/applied/rest-client/index.md b/ai/nextjs-style-guide/applied/rest-client/index.md new file mode 100644 index 0000000..555fb39 --- /dev/null +++ b/ai/nextjs-style-guide/applied/rest-client/index.md @@ -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/`. diff --git a/ai/nextjs-style-guide/data/rest/clients/auto.md b/ai/nextjs-style-guide/applied/rest-client/setup/auto.md similarity index 57% rename from ai/nextjs-style-guide/data/rest/clients/auto.md rename to ai/nextjs-style-guide/applied/rest-client/setup/auto.md index 5098f16..431cd6c 100644 --- a/ai/nextjs-style-guide/data/rest/clients/auto.md +++ b/ai/nextjs-style-guide/applied/rest-client/setup/auto.md @@ -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`. +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, +) => { + const key = getPetDetailKey(params) + const fetcher = () => petStoreApi.pet.getPetById(params as GetPetByIdParams) + + return useSWR(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. diff --git a/ai/nextjs-style-guide/applied/rest-client/setup/hooks.md b/ai/nextjs-style-guide/applied/rest-client/setup/hooks.md new file mode 100644 index 0000000..73605a2 --- /dev/null +++ b/ai/nextjs-style-guide/applied/rest-client/setup/hooks.md @@ -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`. +- Для GET-метода без параметров хук принимает только `config?: SWRConfiguration`. +- 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, +) => { + const key = getPetListKey(params) + const fetcher = () => petStoreApi.pet.findPetsByStatus( + params as FindPetsByStatusParams, + ) + + return useSWR(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, +) => { + const key = getPetDetailKey(params) + const fetcher = () => petStoreApi.pet.getPetById(params as GetPetByIdParams) + + return useSWR(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, +) => { + return useSWR( + getStoreInventoryKey(), + () => petStoreApi.store.getInventory(), + config, + ) +} +``` + +Если generated-метод возвращает безымянный тип вроде `Record`, а тип нужен наружу, вынесите его в `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(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). diff --git a/ai/nextjs-style-guide/data/rest/clients/index.md b/ai/nextjs-style-guide/applied/rest-client/setup/index.md similarity index 61% rename from ai/nextjs-style-guide/data/rest/clients/index.md rename to ai/nextjs-style-guide/applied/rest-client/setup/index.md index b0e8ff8..30a841a 100644 --- a/ai/nextjs-style-guide/data/rest/clients/index.md +++ b/ai/nextjs-style-guide/applied/rest-client/setup/index.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`. + Подробности: - [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`, а этот тип нужен снаружи, вынесите его в `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). diff --git a/ai/nextjs-style-guide/data/rest/clients/manual.md b/ai/nextjs-style-guide/applied/rest-client/setup/manual.md similarity index 89% rename from ai/nextjs-style-guide/data/rest/clients/manual.md rename to ai/nextjs-style-guide/applied/rest-client/setup/manual.md index cb9820b..a07da4c 100644 --- a/ai/nextjs-style-guide/data/rest/clients/manual.md +++ b/ai/nextjs-style-guide/applied/rest-client/setup/manual.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). diff --git a/ai/nextjs-style-guide/applied/rest-client/usage.md b/ai/nextjs-style-guide/applied/rest-client/usage.md new file mode 100644 index 0000000..d4b2097 --- /dev/null +++ b/ai/nextjs-style-guide/applied/rest-client/usage.md @@ -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) +} +``` diff --git a/ai/nextjs-style-guide/data/index.md b/ai/nextjs-style-guide/data/index.md deleted file mode 100644 index 75b1a09..0000000 --- a/ai/nextjs-style-guide/data/index.md +++ /dev/null @@ -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. Отдельный прикладной раздел для них пока не ведётся. diff --git a/ai/nextjs-style-guide/data/realtime.md b/ai/nextjs-style-guide/data/realtime.md deleted file mode 100644 index f284c2b..0000000 --- a/ai/nextjs-style-guide/data/realtime.md +++ /dev/null @@ -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 {count ?? 0} -} -``` - -Плюсы: кеш и дедупликация подписки между несколькими местами рендера; единая модель данных с 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/`. - -Исключения — точечные и обоснованные (например, диагностический скрипт), помечаются комментарием. diff --git a/ai/nextjs-style-guide/data/rest/clients/hooks.md b/ai/nextjs-style-guide/data/rest/clients/hooks.md deleted file mode 100644 index 7b4f271..0000000 --- a/ai/nextjs-style-guide/data/rest/clients/hooks.md +++ /dev/null @@ -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(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 ( - - {children} - - ) -} -``` - -Клиентский компонент при этом ничего не знает про 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(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). diff --git a/ai/nextjs-style-guide/data/rest/index.md b/ai/nextjs-style-guide/data/rest/index.md deleted file mode 100644 index 174a7d5..0000000 --- a/ai/nextjs-style-guide/data/rest/index.md +++ /dev/null @@ -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/`.