From 3d93efd90af6e2495c9b5ead61a5386d18b71870 Mon Sep 17 00:00:00 2001 From: "S.Gromov" Date: Mon, 27 Apr 2026 00:54:26 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=20=C2=AB?= =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=C2=BB=20=D0=B8=20=D1=80?= =?UTF-8?q?=D0=B5=D0=BE=D1=80=D0=B3=D0=B0=D0=BD=D0=B8=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=D1=86=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлен раздел «Данные»: REST (автоматическая и ручная генерация клиентов, получение данных в server и client компонентах с инкапсуляцией SWR в хуках), Realtime, введение - Прикладные разделы переименованы в «Использование», папка перенесена в `docs/docs/usage/` - Создана группа «Установка и настройка» с папкой `docs/docs/setup/` — туда вынесены PostCSS, Biome, VS Code, алиасы и установка SVG-спрайтов - Подгруппы «Стили» и «SVG-спрайты» в сайдбаре упразднены — страницы установки и использования разнесены по верхнеуровневым группам - Удалён устаревший раздел `applied/api.md` - Перекрёстные ссылки в workflow-разделах и внутри новых страниц синхронизированы с новыми путями - CONTRIBUTING.md обновлён под новую структуру папок --- .vitepress/config.ts | 85 ++-- CONTRIBUTING.md | 13 +- docs/docs/applied/api.md | 0 docs/docs/{applied => setup}/aliases.md | 0 docs/docs/{applied => setup}/biome.md | 2 +- .../docs/{applied/styles => setup}/postcss.md | 4 +- .../setup.md => setup/svg-sprites.md} | 4 +- docs/docs/{applied => setup}/vscode.md | 0 docs/docs/{applied => usage}/components.md | 0 docs/docs/usage/data/index.md | 50 +++ docs/docs/usage/data/realtime.md | 80 ++++ docs/docs/usage/data/rest/clients/auto.md | 280 ++++++++++++++ docs/docs/usage/data/rest/clients/manual.md | 366 ++++++++++++++++++ docs/docs/usage/data/rest/fetching/client.md | 165 ++++++++ docs/docs/usage/data/rest/fetching/server.md | 67 ++++ docs/docs/{applied => usage}/fonts.md | 0 docs/docs/{applied => usage}/hooks.md | 0 .../docs/{applied => usage}/images-sprites.md | 0 docs/docs/{applied => usage}/localization.md | 0 docs/docs/{applied => usage}/page-level.md | 0 .../{applied => usage}/project-structure.md | 0 docs/docs/{applied => usage}/stores.md | 0 .../styles/usage.md => usage/styles.md} | 2 +- .../usage.md => usage/svg-sprites.md} | 2 +- .../templates-generation.md | 0 docs/docs/{applied => usage}/video.md | 0 docs/docs/workflow/code-generation.md | 2 +- docs/docs/workflow/creating-components.md | 6 +- docs/docs/workflow/creating-pages.md | 6 +- docs/docs/workflow/getting-started.md | 2 +- docs/docs/workflow/styling.md | 2 +- 31 files changed, 1087 insertions(+), 51 deletions(-) delete mode 100644 docs/docs/applied/api.md rename docs/docs/{applied => setup}/aliases.md (100%) rename docs/docs/{applied => setup}/biome.md (98%) rename docs/docs/{applied/styles => setup}/postcss.md (95%) rename docs/docs/{applied/svg-sprites/setup.md => setup/svg-sprites.md} (93%) rename docs/docs/{applied => setup}/vscode.md (100%) rename docs/docs/{applied => usage}/components.md (100%) create mode 100644 docs/docs/usage/data/index.md create mode 100644 docs/docs/usage/data/realtime.md create mode 100644 docs/docs/usage/data/rest/clients/auto.md create mode 100644 docs/docs/usage/data/rest/clients/manual.md create mode 100644 docs/docs/usage/data/rest/fetching/client.md create mode 100644 docs/docs/usage/data/rest/fetching/server.md rename docs/docs/{applied => usage}/fonts.md (100%) rename docs/docs/{applied => usage}/hooks.md (100%) rename docs/docs/{applied => usage}/images-sprites.md (100%) rename docs/docs/{applied => usage}/localization.md (100%) rename docs/docs/{applied => usage}/page-level.md (100%) rename docs/docs/{applied => usage}/project-structure.md (100%) rename docs/docs/{applied => usage}/stores.md (100%) rename docs/docs/{applied/styles/usage.md => usage/styles.md} (98%) rename docs/docs/{applied/svg-sprites/usage.md => usage/svg-sprites.md} (98%) rename docs/docs/{applied => usage}/templates-generation.md (100%) rename docs/docs/{applied => usage}/video.md (100%) diff --git a/.vitepress/config.ts b/.vitepress/config.ts index e243bef..f1d7496 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -30,40 +30,63 @@ const sidebar = [ ], }, { - text: 'Прикладные разделы', + text: 'Установка и настройка', items: [ - { text: 'Структура проекта', link: '/docs/applied/project-structure' }, - { text: 'Алиасы', link: '/docs/applied/aliases' }, - { text: 'Компоненты', link: '/docs/applied/components' }, - { text: 'Страницы (App Router)', link: '/docs/applied/page-level' }, - { text: 'Шаблоны и генерация кода', link: '/docs/applied/templates-generation' }, - { - text: 'Стили', - collapsed: true, - items: [ - { text: 'PostCSS', link: '/docs/applied/styles/postcss' }, - { text: 'Использование', link: '/docs/applied/styles/usage' }, - ], - }, - { text: 'Изображения', link: '/docs/applied/images-sprites' }, - { - text: 'SVG-спрайты', - collapsed: true, - items: [ - { text: 'Установка и настройка', link: '/docs/applied/svg-sprites/setup' }, - { text: 'Использование', link: '/docs/applied/svg-sprites/usage' }, - ], - }, - { text: 'Видео', link: '/docs/applied/video' }, - { text: 'API', link: '/docs/applied/api' }, - { text: 'Stores', link: '/docs/applied/stores' }, - { text: 'Хуки', link: '/docs/applied/hooks' }, - { text: 'Шрифты', link: '/docs/applied/fonts' }, - { text: 'Локализация', link: '/docs/applied/localization' }, - { text: 'Biome', link: '/docs/applied/biome' }, - { text: 'Настройка VS Code', link: '/docs/applied/vscode' }, + { text: 'Алиасы', link: '/docs/setup/aliases' }, + { text: 'Biome', link: '/docs/setup/biome' }, + { text: 'PostCSS', link: '/docs/setup/postcss' }, + { text: 'SVG-спрайты', link: '/docs/setup/svg-sprites' }, + { text: 'VS Code', link: '/docs/setup/vscode' }, ], }, + { + text: 'Использование', + items: [ + { text: 'Структура проекта', link: '/docs/usage/project-structure' }, + { text: 'Компоненты', link: '/docs/usage/components' }, + { text: 'Страницы (App Router)', link: '/docs/usage/page-level' }, + { text: 'Шаблоны и генерация кода', link: '/docs/usage/templates-generation' }, + { text: 'Стили', link: '/docs/usage/styles' }, + { text: 'Изображения', link: '/docs/usage/images-sprites' }, + { text: 'SVG-спрайты', link: '/docs/usage/svg-sprites' }, + { text: 'Видео', link: '/docs/usage/video' }, + { + text: 'Данные', + collapsed: true, + items: [ + { text: 'Введение', link: '/docs/usage/data/' }, + { + text: 'REST', + collapsed: true, + items: [ + { + text: 'Клиенты', + collapsed: true, + items: [ + { text: 'Автоматическая генерация', link: '/docs/usage/data/rest/clients/auto' }, + { text: 'Ручная генерация', link: '/docs/usage/data/rest/clients/manual' }, + ], + }, + { + text: 'Получение данных', + collapsed: true, + items: [ + { text: 'Серверные компоненты', link: '/docs/usage/data/rest/fetching/server' }, + { text: 'Клиентские компоненты', link: '/docs/usage/data/rest/fetching/client' }, + ], + }, + ], + }, + { text: 'Realtime', link: '/docs/usage/data/realtime' }, + ], + }, + { text: 'Stores', link: '/docs/usage/stores' }, + { text: 'Хуки', link: '/docs/usage/hooks' }, + { text: 'Шрифты', link: '/docs/usage/fonts' }, + { text: 'Локализация', link: '/docs/usage/localization' }, + ], + }, + ]; /** diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f33eac3..df87192 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,8 +34,13 @@ docs/ │ ├── naming.md │ ├── documentation.md │ └── typing.md - └── applied/ # Прикладные разделы - ├── vscode.md + ├── setup/ # Установка: разовая настройка проекта + │ ├── aliases.md + │ ├── biome.md + │ ├── postcss.md + │ ├── svg-sprites.md + │ └── vscode.md + └── usage/ # Использование: повседневная работа ├── project-structure.md ├── components.md ├── page-level.md @@ -44,7 +49,7 @@ docs/ ├── images-sprites.md ├── svg-sprites.md ├── video.md - ├── api.md + ├── data/ ├── stores.md ├── hooks.md ├── fonts.md @@ -59,7 +64,7 @@ generate-llms.ts # Скрипт генерации llms.txt и R ### Добавление нового раздела -1. Создать `.md`-файл в нужной папке (`docs/docs/basics/` или `docs/docs/applied/`). +1. Создать `.md`-файл в нужной папке (`docs/docs/basics/`, `docs/docs/setup/` или `docs/docs/usage/`). 2. Добавить пункт в сайдбар — `.vitepress/config.ts`. Сайдбар — единственный источник порядка и группировки для `llms.txt`. 3. Запустить `npm run llms` для обновления `llms.txt` и README. diff --git a/docs/docs/applied/api.md b/docs/docs/applied/api.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/docs/applied/aliases.md b/docs/docs/setup/aliases.md similarity index 100% rename from docs/docs/applied/aliases.md rename to docs/docs/setup/aliases.md diff --git a/docs/docs/applied/biome.md b/docs/docs/setup/biome.md similarity index 98% rename from docs/docs/applied/biome.md rename to docs/docs/setup/biome.md index 32c6253..e96389f 100644 --- a/docs/docs/applied/biome.md +++ b/docs/docs/setup/biome.md @@ -77,4 +77,4 @@ keywords: [biome, линтер, форматтер, lint, format, biome.json, "@ ## Интеграция с VS Code -Расширение `biomejs.biome` и автоформатирование при сохранении настраиваются в [Настройка VS Code](/docs/applied/vscode). +Расширение `biomejs.biome` и автоформатирование при сохранении настраиваются в [Настройка VS Code](/docs/setup/vscode). diff --git a/docs/docs/applied/styles/postcss.md b/docs/docs/setup/postcss.md similarity index 95% rename from docs/docs/applied/styles/postcss.md rename to docs/docs/setup/postcss.md index 8200c87..3f0e120 100644 --- a/docs/docs/applied/styles/postcss.md +++ b/docs/docs/setup/postcss.md @@ -7,7 +7,7 @@ keywords: [postcss, postcss.config.mjs, postcss-custom-media, postcss-nesting, a Установка и настройка CSS-процессора PostCSS в проекте: набор плагинов, конфиг `postcss.config.mjs`. Выполняется один раз при заведении проекта. -Правила написания CSS в компонентах — [Использование](/docs/applied/styles/usage). +Правила написания CSS в компонентах — [Использование](/docs/usage/styles). ## Зачем PostCSS @@ -68,4 +68,4 @@ export default { Опция `importFrom` у `postcss-custom-media` удалена в v10+; её роль теперь выполняет `@csstools/postcss-global-data`. -Импортировать `media.css` в файлы компонентов **не нужно** и запрещено правилами (см. [Использование](/docs/applied/styles/usage), раздел «Импорт стилей»). +Импортировать `media.css` в файлы компонентов **не нужно** и запрещено правилами (см. [Использование](/docs/usage/styles), раздел «Импорт стилей»). diff --git a/docs/docs/applied/svg-sprites/setup.md b/docs/docs/setup/svg-sprites.md similarity index 93% rename from docs/docs/applied/svg-sprites/setup.md rename to docs/docs/setup/svg-sprites.md index e1f5eee..b1f63fc 100644 --- a/docs/docs/applied/svg-sprites/setup.md +++ b/docs/docs/setup/svg-sprites.md @@ -7,7 +7,7 @@ keywords: [svg-sprites, установка, настройка, config, паке Первичная настройка пакета `@gromlab/svg-sprites` в проекте. Выполняется один раз при заведении проекта и при смене мажорной версии пакета. -Что такое спрайты, как с ними работать и как управлять цветом — [Использование](/docs/applied/svg-sprites/usage). +Что такое спрайты, как с ними работать и как управлять цветом — [Использование](/docs/usage/svg-sprites). ## Требования @@ -30,7 +30,7 @@ keywords: [svg-sprites, установка, настройка, config, паке mkdir -p src/shared/sprites/icons ``` - Источники спрайтов живут в `src/shared/sprites//` — это слой `shared` SLM-архитектуры (см. [Структура проекта](/docs/applied/project-structure), [Архитектура](/docs/basics/architecture/)). В `src/` посторонних каталогов вне слоёв не заводим. + Источники спрайтов живут в `src/shared/sprites//` — это слой `shared` SLM-архитектуры (см. [Структура проекта](/docs/usage/project-structure), [Архитектура](/docs/basics/architecture/)). В `src/` посторонних каталогов вне слоёв не заводим. 4. Добавить скрипты в `package.json`: diff --git a/docs/docs/applied/vscode.md b/docs/docs/setup/vscode.md similarity index 100% rename from docs/docs/applied/vscode.md rename to docs/docs/setup/vscode.md diff --git a/docs/docs/applied/components.md b/docs/docs/usage/components.md similarity index 100% rename from docs/docs/applied/components.md rename to docs/docs/usage/components.md diff --git a/docs/docs/usage/data/index.md b/docs/docs/usage/data/index.md new file mode 100644 index 0000000..1b4759e --- /dev/null +++ b/docs/docs/usage/data/index.md @@ -0,0 +1,50 @@ +--- +title: Введение +keywords: [данные, api, rest, realtime, клиент, swr, infrastructure, введение, карта раздела] +--- + +# Введение + +Работа с источниками данных в проекте: REST, realtime и любые другие каналы, которые появятся в будущем. Раздел описывает, как создаются клиенты для API и как полученные данные доходят до страниц и компонентов. + +## Принципы раздела + +- **Клиент — в `infrastructure/`.** Каждый внешний сервис — отдельный модуль слоя `infrastructure/{service-name}/`. +- **Прямой `fetch` запрещён.** Запросы идут только через клиент модуля. Исключения — точечные и обоснованные. +- **Источник данных диктует канал.** REST, realtime и т.п. — независимые подразделы, у каждого своя модель клиента и своё потребление. +- **Серверные и клиентские компоненты потребляют по-разному.** Server Components — прямой `await` метода клиента, клиентские — через готовые хуки модуля API (`useUserList`, `usePostDetail` и т.п.). SWR инкапсулирован в хуке, компонент про него не знает. + +## Карта раздела + +### REST + +Канал «запрос-ответ» по HTTP. Покрывает большинство API. + +- **Клиенты** — как создаётся клиент REST API: + - [Автоматическая генерация](/docs/usage/data/rest/clients/auto) — для API с OpenAPI-спецификацией, через `@gromlab/api-codegen`. + - [Ручная генерация](/docs/usage/data/rest/clients/manual) — для API без схемы, клиент пишется и поддерживается руками. +- **Получение данных** — как клиент используется в приложении: + - [Серверные компоненты](/docs/usage/data/rest/fetching/server) — прямой `await` метода клиента в Server Components. + - [Клиентские компоненты](/docs/usage/data/rest/fetching/client) — через готовые хуки модуля API; SWR с кешем, дедупликацией и ревалидацией скрыт внутри хука. + +### Realtime + +Канал push-данных: WebSocket, SSE, событийные шины. Транспорт не зашит в правила — важна абстракция «подписка». + +- [Realtime](/docs/usage/data/realtime) — клиент realtime в `infrastructure/`, потребление через `useSWRSubscription` или прямые подписки. + +## Что даёт раздел + +После прочтения раздела понятно: + +- Где живёт код работы с API и почему именно там. +- Когда генерировать клиент автоматически, а когда писать вручную, и как структурирован каждый из вариантов. +- Как получать данные на сервере и на клиенте, чтобы не ломать кеш и не плодить лишние запросы. +- Как подключать realtime-источники в общую модель работы с данными. +- Какие правила обязательны и какие отклонения допустимы. + +## Что не входит в раздел + +- **Глобальное состояние UI** — Stores, формы, фичефлаги. Это [Stores](/docs/usage/stores). +- **Доменная логика** — как данные превращаются в сценарии бизнеса. Это слой `business/` в [Архитектуре](/docs/basics/architecture/). +- **Хуки общего назначения** — переиспользуемые хуки UI, не привязанные к конкретному API. Это [Хуки](/docs/usage/hooks). diff --git a/docs/docs/usage/data/realtime.md b/docs/docs/usage/data/realtime.md new file mode 100644 index 0000000..8d736e0 --- /dev/null +++ b/docs/docs/usage/data/realtime.md @@ -0,0 +1,80 @@ +--- +title: Realtime +keywords: [realtime, websocket, sse, подписка, swr subscription, useSWRSubscription, push, события] +--- + +# Realtime + +Канал для push-данных: WebSocket, SSE, событийные шины и любой другой источник, инициирующий передачу со стороны сервера. Транспорт не зашит в правила — важна абстракция «подписка». + +Получение REST-данных — [REST](/docs/usage/data/rest/clients/auto). + +## Принципы + +- **Клиент realtime — в `infrastructure/`** отдельным модулем по имени канала. То же правило, что и для REST: никаких прямых соединений в коде приложения. +- **Подписка — единица потребления.** Клиент даёт функцию `subscribe(topic, handler) → unsubscribe`. Внутри — конкретный транспорт. +- **Использование на клиенте — два сценария:** + - **`useSWRSubscription`** — для данных, которые показываются в UI и должны кешироваться/синхронизироваться с REST. + - **Прямая подписка** — для побочных эффектов (тосты, нотификации, аналитика), не привязанных к рендеру. + +## Размещение клиента + +```text +src/infrastructure/ +└── {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 'infrastructure/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 'infrastructure/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(...)` или подписываться на событийные шины напрямую в коде приложения — запрещено. Все соединения проходят через клиент в `infrastructure/`. + +Исключения — точечные и обоснованные (например, диагностический скрипт), помечаются комментарием. diff --git a/docs/docs/usage/data/rest/clients/auto.md b/docs/docs/usage/data/rest/clients/auto.md new file mode 100644 index 0000000..cd420fc --- /dev/null +++ b/docs/docs/usage/data/rest/clients/auto.md @@ -0,0 +1,280 @@ +--- +title: Автоматическая генерация +keywords: [api, rest, openapi, codegen, генерация, клиент, api-codegen, gromlab, infrastructure, swagger-typescript-api] +--- + +# Автоматическая генерация + +Если у API есть OpenAPI-спецификация — клиент генерируется утилитой [@gromlab/api-codegen](https://gromlab.ru/gromov/api-codegen) (обёртка над `swagger-typescript-api`). Ручной код для таких API не пишется. + +Когда схемы нет — [Ручная генерация](/docs/usage/data/rest/clients/manual). + +В примерах ниже используется условный API `pet-project-api` (kebab-case в путях) / `petProjectApi` (camelCase в коде). В реальном проекте имена выбираются по конкретному API. + +## Установка + +```bash +npm install -D @gromlab/api-codegen +``` + +Скрипт генерации в `package.json` — по одному на каждый API: + +```json +{ + "scripts": { + "codegen:pet-project-api": "api-codegen --config src/infrastructure/pet-project-api/config/pet-project-api.config.ts" + } +} +``` + +Конфиг и опции — в репозитории [@gromlab/api-codegen](https://gromlab.ru/gromov/api-codegen). + +## Структура модуля + +Клиент кладётся в слой `infrastructure/` отдельным модулем по имени API (kebab-case): + +```text +src/infrastructure/ +└── pet-project-api/ + ├── generated/ # сегмент сгенерированного кода + │ └── pet-project-api.generated.ts # сгенерировано — не править + ├── types/ # расширения сгенерированных типов + │ ├── user.ts # declare module + Extended-тип + │ └── index.ts # реэкспорт расширений + ├── hooks/ # SWR-хуки для клиентских компонентов + │ ├── use-user-list.hook.ts + │ ├── use-user-detail.hook.ts + │ └── index.ts # реэкспорт хуков + ├── config/ # конфиги модуля + │ └── pet-project-api.config.ts # конфиг генерации клиента + ├── client.ts # настройка HttpClient, инстанс Api + └── index.ts # публичный API модуля +``` + +| Файл | Роль | Кто правит | +|------|------|-----------| +| `generated/{service-name}.generated.ts` | Сгенерированный код: типы, `class Api`, `class HttpClient` | codegen, не править | +| `types/{сущность}.ts` | `declare module` + `Extended`-типы по сущности | разработчик | +| `types/index.ts` | Реэкспорт публичных расширений | разработчик | +| `hooks/use-{action}.hook.ts` | SWR-хук поверх метода клиента | разработчик | +| `hooks/index.ts` | Реэкспорт хуков | разработчик | +| `config/{service-name}.config.ts` | Параметры генерации для конкретного API | разработчик | +| `client.ts` | `baseUrl` из env, конфиг `HttpClient`, инстанс `new Api(...)` | разработчик | +| `index.ts` | Публичный API: инстанс сервиса, расширенные типы, хуки | разработчик | + +`client.ts` и `index.ts` — единственные корневые файлы модуля. Все остальные файлы живут в сегментах (`generated/`, `types/`, `hooks/`, `config/`). + +Имя сгенерированного файла — `{service-name}.generated.ts` (имя сервиса в kebab-case + суффикс `.generated.ts`). Суффикс сигнализирует «не править руками». + +## `client.ts` + +Тонкий ручной слой поверх сгенерированного кода. Делает три вещи: читает и нормализует `baseUrl`, конфигурирует `HttpClient`, создаёт **именованный инстанс** сервиса. + +```ts +// src/infrastructure/pet-project-api/client.ts +import { Api, HttpClient } from './generated/pet-project-api.generated' + +const resolvedBaseUrl = process.env.NEXT_PUBLIC_API_URL + .replace(/\/+$/, '') // убираем хвостовой слэш + .replace(/\/v1$/, '') // версия уже в путях методов — режем дубль + +const httpClient = new HttpClient({ + baseApiParams: { + secure: false, + headers: { + 'Content-Type': 'application/json', + // кастомные заголовки API — если требуются + // 'X-App-Key': '...', + }, + }, +}) + +httpClient.baseUrl = resolvedBaseUrl + +export const petProjectApi = new Api(httpClient) +``` + +### Имя инстанса = имя сервиса + +Инстанс называется по имени API в camelCase, не унифицированно `api`/`client`. Это даёт **процедурное обращение** и однозначность при работе с несколькими сервисами: + +```ts +import { petProjectApi } from 'infrastructure/pet-project-api' + +const user = await petProjectApi.user.getUser(id) +``` + +При нескольких API — каждый со своим именем: + +```ts +import { petProjectApi } from 'infrastructure/pet-project-api' +import { paymentsApi } from 'infrastructure/payments-api' + +const user = await petProjectApi.user.list() +const invoice = await paymentsApi.invoices.list() +``` + +### Нормализация `baseUrl` + +`@gromlab/api-codegen` может включать версию (`/v1`) в `baseUrl` сгенерированного кода, а пути методов уже содержат её — отсюда дубль. Стандартный приём: + +```ts +.replace(/\/+$/, '') // хвостовой слэш +.replace(/\/v1$/, '') // версия (если фигурирует в путях) +``` + +Подгоняется под конкретный API: если версия в путях не повторяется — второй `replace` не нужен. + +## Расширения типов + +Автогенерация не покрывает все реальные поля API: иногда тип `object`, иногда поле просто отсутствует. Расширения живут в `types/`, по файлу на сущность. + +Две техники: + +### `declare module` — добавление полей + +Дополняет существующий интерфейс из `generated.ts`. Сама сгенерированная декларация не трогается. + +```ts +// src/infrastructure/pet-project-api/types/user.ts +import type { User } from '../generated/pet-project-api.generated' + +declare module '../generated/pet-project-api.generated' { + interface User { + avatar?: { + file?: string + title?: string + url?: string + } + } +} +``` + +### `Extended` через `Omit & {...}` — переопределение полей + +Когда автогенерация даёт `object` или общий тип, а реально структура известна — создаётся отдельный тип `UserExtended` (по имени сущности + суффикс `Extended`). + +```ts +// src/infrastructure/pet-project-api/types/user.ts +export type UserExtended = Omit & { + roles?: Array<{ _id?: string; id?: string; slug?: string; name?: string }> + tags?: Array<{ _id?: string; id?: string; slug?: string; name?: string }> + fields?: Record +} +``` + +### Реэкспорт + +```ts +// src/infrastructure/pet-project-api/types/index.ts +export type { UserExtended } from './user' +``` + +### Правила + +- Расширения — **только в `types/`**, не в `client.ts` и не в сгенерированном файле. +- Один файл на сущность (имя файла — kebab-case по сущности: `user.ts`, `order.ts`, `invoice.ts`). +- При регенерации `generated/{service-name}.generated.ts` файлы в `types/` не затрагиваются. +- Если сломался `Extended`-тип после regen — синхронизировать руками. + +## Хуки для клиентских компонентов + +В клиентских компонентах вызовы клиента не делаются напрямую — компонент получает готовый хук, который инкапсулирует SWR + метод клиента. Хуки живут в сегменте `hooks/`, по файлу на операцию. + +```ts +// src/infrastructure/pet-project-api/hooks/use-user-list.hook.ts +import useSWR from 'swr' +import type { SWRConfiguration } from 'swr' +import { petProjectApi } from '../client' +import type { User } from '../generated/pet-project-api.generated' + +/** + * Получение списка пользователей. + */ +export const useUserList = ( + query?: { limit?: number; offset?: number }, + config?: SWRConfiguration, +) => { + return useSWR( + ['pet-project-api', 'user', 'list', query], + () => petProjectApi.user.list(query ?? {}), + config, + ) +} +``` + +```ts +// src/infrastructure/pet-project-api/hooks/use-user-detail.hook.ts +import useSWR from 'swr' +import type { SWRConfiguration } from 'swr' +import { petProjectApi } from '../client' +import type { UserExtended } from '../types' + +/** + * Получение пользователя по идентификатору. + */ +export const useUserDetail = ( + id: string | null, + config?: SWRConfiguration, +) => { + const key = id ? ['pet-project-api', 'user', 'detail', id] : null + const fetcher = () => petProjectApi.user.getUser(id!) as Promise + + return useSWR(key, fetcher, config) +} +``` + +```ts +// src/infrastructure/pet-project-api/hooks/index.ts +export { useUserList } from './use-user-list.hook' +export { useUserDetail } from './use-user-detail.hook' +``` + +### Правила хуков + +- **Один файл — один хук**, имя файла `use-{action}.hook.ts` ([Именование](/docs/basics/naming)). +- **Тонкая обёртка над SWR.** Внутри — построение ключа, fetcher через метод клиента, возврат `useSWR(...)`. Никакой бизнес-логики. +- **Ключ начинается с имени сервиса** (`['pet-project-api', ...]`) — изолирует кеш между разными API. +- **Условный ключ для опциональных параметров:** `id ? [...key, id] : null`. `null` приостанавливает запрос, пока параметры не готовы. +- **Параметр `config?: SWRConfiguration`** — даёт потребителю переопределить ревалидацию, `fallbackData`, `suspense` и т.п. без обёрток. + +## Публичный API модуля + +Из `index.ts` экспортируются инстанс, расширенные типы и хуки. Сырые типы из `generated/` экспортируются по необходимости — точечно. + +```ts +// src/infrastructure/pet-project-api/index.ts +export { petProjectApi } from './client' +export type { UserExtended } from './types' +export * from './hooks' +``` + +## Регенерация + +При изменении OpenAPI-схемы: + +```bash +npm run codegen:pet-project-api +``` + +Что меняется: + +- `generated/{service-name}.generated.ts` — перезаписывается полностью, изменения коммитятся. +- `client.ts`, `types/`, `config/`, `index.ts` — **не трогаются** автоматически. + +Поломка контракта (изменение типов в схеме) ловится TypeScript при сборке проекта. Если ломаются `Extended`-типы — синхронизировать вручную в соответствующих файлах `types/`. + +## Сгенерированный файл коммитится + +Файл `generated/{service-name}.generated.ts` **не добавляется в `.gitignore`** — попадает в репозиторий вместе с остальным кодом. + +Причины: + +- **Детерминированная сборка.** `npm run build` не зависит от доступности OpenAPI-схемы (обычно она на удалённом сервере). Сервис лёг — прод собирается. +- **Видимость изменений в PR.** Diff показывает, что именно поменялось в контракте API между версиями. +- **Простой онбординг.** После `git clone` IDE сразу видит типы, без предварительной генерации. +- **Фиксация версии контракта.** Пересборка старого коммита даёт ровно тот клиент, что был тогда. + +Регенерация — **ручная команда** при обновлении схемы, не хук `predev`/`prebuild`. Запускается осознанно. + +Исключение возможно, только если OpenAPI-схема лежит **в этом же репозитории** и генерация быстрая, без сети — тогда допустимо добавить сегмент `generated/` в `.gitignore` и хук `prebuild`, по аналогии со спрайтами. На практике встречается редко. diff --git a/docs/docs/usage/data/rest/clients/manual.md b/docs/docs/usage/data/rest/clients/manual.md new file mode 100644 index 0000000..698ab7f --- /dev/null +++ b/docs/docs/usage/data/rest/clients/manual.md @@ -0,0 +1,366 @@ +--- +title: Ручная генерация +keywords: [api, rest, клиент, ручной, fetch, infrastructure, api-клиент] +--- + +# Ручная генерация + +Если у API нет OpenAPI-спецификации — клиент пишется и поддерживается вручную. Цель та же, что и у автогенерации: единая точка работы с API, без прямых `fetch` в коде приложения. + +Когда схема есть — [Автоматическая генерация](/docs/usage/data/rest/clients/auto). + +В примерах ниже используется условный API `pet-project-api` / `petProjectApi`. В реальном проекте имена выбираются по конкретному API. + +## Структура модуля + +Клиент живёт в слое `infrastructure/` отдельным модулем по имени API (kebab-case): + +```text +src/infrastructure/ +└── pet-project-api/ + ├── methods/ # методы по сущностям API + │ ├── pages.ts + │ ├── posts.ts + │ └── forms.ts + ├── hooks/ # SWR-хуки для клиентских компонентов + │ ├── use-post-detail.hook.ts + │ ├── use-post-filter.hook.ts + │ └── index.ts + ├── types/ # типы клиента и доменные типы + │ ├── client.ts # типы клиента: RequestOptions, ParamValue + │ ├── post.ts # доменные типы сущности post + │ ├── form.ts # доменные типы сущности form + │ └── index.ts # реэкспорт публичных типов + ├── errors/ # доменные ошибки API + │ └── pet-project-api.error.ts + ├── client.ts # класс клиента: baseUrl, headers, get/post + └── index.ts # публичный API модуля +``` + +| Файл | Роль | +|------|------| +| `client.ts` | Класс `PetProjectApiClient`: `baseUrl`, общие заголовки, `buildUrl`, базовые `get`/`post` | +| `methods/{entity}.ts` | Методы по сущности, экспортируются фабрикой `{entity}Methods(client)` | +| `hooks/use-{action}.hook.ts` | SWR-хук поверх метода клиента | +| `hooks/index.ts` | Реэкспорт хуков | +| `types/client.ts` | Типы инфраструктуры клиента: `RequestOptions`, `PostOptions`, `ParamValue` | +| `types/{entity}.ts` | Доменные типы: запросы, ответы, фильтры по сущности | +| `types/index.ts` | Реэкспорт публичных типов | +| `errors/{service-name}.error.ts` | Доменный класс ошибок API | +| `index.ts` | Публичный API: инстанс клиента, хуки, доменные ошибки, типы | + +`methods/`, `hooks/`, `types/`, `errors/` — сегменты модуля по канону SLM. `client.ts` и `index.ts` — единственные корневые файлы. + +## Типы клиента + +Типы, описывающие саму инфраструктуру запросов (опции, параметры) — выносятся в `types/client.ts`. Это держит `client.ts` коротким и не смешивает декларации типов с реализацией класса. + +```ts +// src/infrastructure/pet-project-api/types/client.ts +export type ParamValue = string | number | (string | number)[] + +export type RequestOptions = { + params?: Record + headers?: Record + revalidate?: number | false +} + +export type PostOptions = RequestOptions & { + type?: 'json' | 'formdata' +} +``` + +## Базовый клиент + +Класс с конфигурацией (`baseUrl`, общие заголовки) и базовыми методами `get` / `post`. Конкретные методы API размещаются в сегменте `methods/`, а не на самом классе — это держит `client.ts` коротким и не плодит «бога-класс». + +```ts +// src/infrastructure/pet-project-api/client.ts +import { PetProjectApiError } from './errors/pet-project-api.error' +import type { ParamValue, RequestOptions, PostOptions } from './types/client' + +export class PetProjectApiClient { + constructor( + private readonly baseUrl: string, + private readonly defaultHeaders: Record = {}, + ) { + this.defaultHeaders = { + Accept: 'application/json', + ...defaultHeaders, + } + } + + buildUrl(path: string, params?: Record): string { + const base = this.baseUrl.replace(/\/+$/, '') + const tail = path.replace(/^\/+/, '') + const url = `${base}/${tail}` + + if (!params) { + return url + } + + const search = new URLSearchParams() + for (const [key, value] of Object.entries(params)) { + if (Array.isArray(value)) { + value.forEach((v) => search.append(key, String(v))) + } else { + search.set(key, String(value)) + } + } + + return `${url}?${search}` + } + + async get(path: string, options: RequestOptions = {}): Promise { + const { params, headers, revalidate } = options + const response = await fetch(this.buildUrl(path, params), { + headers: { ...this.defaultHeaders, ...headers }, + ...(revalidate !== undefined && { next: { revalidate } }), + }) + + if (!response.ok) { + throw await PetProjectApiError.fromResponse(response) + } + + return response.json() as Promise + } + + async post(path: string, body: unknown, options: PostOptions = {}): Promise { + const { params, headers, revalidate, type = 'json' } = options + const isJson = type === 'json' + + const response = await fetch(this.buildUrl(path, params), { + method: 'POST', + headers: { + ...this.defaultHeaders, + ...(isJson && { 'Content-Type': 'application/json' }), + ...headers, + }, + body: isJson ? JSON.stringify(body) : (body as BodyInit), + ...(revalidate !== undefined && { next: { revalidate } }), + }) + + if (!response.ok) { + throw await PetProjectApiError.fromResponse(response) + } + + return response.json() as Promise + } +} +``` + +### Ключевые требования к клиенту + +- **Класс с приватным состоянием** (`baseUrl`, `defaultHeaders`) — конфигурация инкапсулирована. +- **Типы клиента — в `types/client.ts`**, не в `client.ts`. Реализация и контракты разделены. +- **Базовые методы дженерик `` без дефолта.** Вызов без типа невозможен — потребитель обязан указать форму ответа. +- **Доменная ошибка вместо `null`.** При не-`ok` бросается `PetProjectApiError`. Возврат `null` глотает причины (404 vs 500 vs 401) — не использовать. +- **Дефолт POST — `json`.** `formdata` указывается явно, на конкретных методах (загрузка файлов, отправка форм). +- **Нормализация слэшей** в `buildUrl` — `baseUrl` без хвостового `/`, `path` без ведущего `/`. +- **`async/await`**, не `.then()` — линейное чтение, простая обработка ошибок. +- **Поддержка `next.revalidate`** — клиент знает о Next.js App Router и пробрасывает кеш-флаги. + +## Доменная ошибка + +Сетевая ошибка превращается в класс ошибки модуля. Наружу не выходит сырой `Response`. + +```ts +// src/infrastructure/pet-project-api/errors/pet-project-api.error.ts +export class PetProjectApiError extends Error { + constructor( + public readonly status: number, + public readonly body: string, + ) { + super(`PetProjectApi ${status}: ${body.slice(0, 200)}`) + this.name = 'PetProjectApiError' + } + + static async fromResponse(response: Response): Promise { + const body = await response.text().catch(() => '') + return new PetProjectApiError(response.status, body) + } +} +``` + +Дополнительные подклассы по необходимости: `PetProjectApiValidationError` (400), `PetProjectApiAuthError` (401/403), `PetProjectApiNotFoundError` (404). Вводятся когда у потребителя есть **разная реакция** на разные коды; иначе хватает базового класса. + +## Доменные типы + +Типы запросов, ответов и фильтров — по файлу на сущность. Типы должны лежать рядом по смыслу: всё, что относится к `posts`, — в `types/post.ts`. + +```ts +// src/infrastructure/pet-project-api/types/post.ts +export type Post = { + id: string + slug: string + title: string + content: string + publishedAt: string +} + +export type PostFilter = { + limit?: number + categories?: number[] +} +``` + +```ts +// src/infrastructure/pet-project-api/types/index.ts +export type * from './post' +export type * from './form' +// типы клиента — внутренние, наружу не реэкспортируются +``` + +Типы клиента (`RequestOptions`, `PostOptions`, `ParamValue`) **не реэкспортируются** через `types/index.ts` — они нужны только внутри модуля. + +## Методы + +Методы группируются по сущностям в сегменте `methods/`, экспортируются фабрикой, принимающей клиент. Это даёт **процедурное обращение** в стиле автогенерированного клиента (`petProjectApi.posts.get(slug)`), а не плоский список (`petProjectApi.getPost(slug)`). + +```ts +// src/infrastructure/pet-project-api/methods/posts.ts +import type { PetProjectApiClient } from '../client' +import type { Post, PostFilter } from '../types/post' + +export function postsMethods(client: PetProjectApiClient) { + return { + /** GET /posts/{slug} */ + get: (slug: string, options?: { revalidate?: number | false }) => + client.get(`posts/${slug}`, options), + + /** POST /posts/filter */ + filter: (body: PostFilter) => + client.post('posts/filter', body), + } +} +``` + +```ts +// src/infrastructure/pet-project-api/methods/forms.ts +import type { PetProjectApiClient } from '../client' +import type { Form, FormSubmissionResult } from '../types/form' + +export function formsMethods(client: PetProjectApiClient) { + return { + /** GET /forms/{id} */ + get: (id: string) => client.get
(`forms/${id}`), + + /** POST /forms/{id} — multipart/form-data */ + submit: (id: string, data: FormData) => + client.post(`forms/${id}`, data, { type: 'formdata' }), + } +} +``` + +### Правила методов + +- **Группировка по сущности** (`pages`, `posts`, `forms`), не плоский список. +- **Имя метода — глагол действия**: `get`, `list`, `filter`, `create`, `update`, `delete`, `submit`. Не `getPost`/`getPosts` — сущность уже в имени группы. +- **Типы запросов и ответов — в `types/{entity}.ts`**, импортируются в файл методов. В `methods/` лежит только композиция вызовов клиента, без объявлений типов. +- **Фабрика принимает клиент** — это даёт тестируемость (моковый клиент в юнит-тестах) и единый источник конфигурации. +- **Никаких знаний об UI.** Клиент не знает про React, SWR, тосты — только данные и ошибки. + +## Сборка инстанса + +Группы методов соединяются в один объект на уровне `index.ts`. Это даёт процедурный доступ `petProjectApi.posts.get(...)`. + +```ts +// src/infrastructure/pet-project-api/index.ts +import { PetProjectApiClient } from './client' +import { pagesMethods } from './methods/pages' +import { postsMethods } from './methods/posts' +import { formsMethods } from './methods/forms' + +const client = new PetProjectApiClient(process.env.NEXT_PUBLIC_API_URL, { + 'X-App-Key': process.env.NEXT_PUBLIC_APP_KEY, +}) + +export const petProjectApi = { + pages: pagesMethods(client), + posts: postsMethods(client), + forms: formsMethods(client), +} + +export { PetProjectApiError } from './errors/pet-project-api.error' +export type { Post, PostFilter, Page, Form } from './types' +export * from './hooks' +``` + +## Хуки для клиентских компонентов + +В клиентских компонентах вызовы клиента не делаются напрямую — компонент получает готовый хук, который инкапсулирует SWR + метод клиента. Хуки живут в сегменте `hooks/`, по файлу на операцию. + +```ts +// src/infrastructure/pet-project-api/hooks/use-post-detail.hook.ts +import useSWR from 'swr' +import type { SWRConfiguration } from 'swr' +import { petProjectApi } from '..' +import type { Post } from '../types/post' + +/** + * Получение поста по slug. + */ +export const usePostDetail = ( + slug: string | null, + config?: SWRConfiguration, +) => { + const key = slug ? ['pet-project-api', 'post', 'detail', slug] : null + const fetcher = () => petProjectApi.posts.get(slug!) + + return useSWR(key, fetcher, config) +} +``` + +```ts +// src/infrastructure/pet-project-api/hooks/use-post-filter.hook.ts +import useSWR from 'swr' +import type { SWRConfiguration } from 'swr' +import { petProjectApi } from '..' +import type { Post, PostFilter } from '../types/post' + +/** + * Получение списка постов по фильтру. + */ +export const usePostFilter = ( + filter: PostFilter, + config?: SWRConfiguration, +) => { + return useSWR( + ['pet-project-api', 'post', 'filter', filter], + () => petProjectApi.posts.filter(filter), + config, + ) +} +``` + +```ts +// src/infrastructure/pet-project-api/hooks/index.ts +export { usePostDetail } from './use-post-detail.hook' +export { usePostFilter } from './use-post-filter.hook' +``` + +### Правила хуков + +- **Один файл — один хук**, имя файла `use-{action}.hook.ts` ([Именование](/docs/basics/naming)). +- **Тонкая обёртка над SWR.** Внутри — построение ключа, fetcher через метод клиента, возврат `useSWR(...)`. Никакой бизнес-логики. +- **Ключ начинается с имени сервиса** (`['pet-project-api', ...]`) — изолирует кеш между разными API. +- **Условный ключ для опциональных параметров:** `id ? [...key, id] : null`. `null` приостанавливает запрос, пока параметры не готовы. +- **Параметр `config?: SWRConfiguration`** — даёт потребителю переопределить ревалидацию, `fallbackData`, `suspense` и т.п. без обёрток. + +## Запрет прямого `fetch` + +В коде приложения (слои выше `infrastructure`) прямые вызовы `fetch` к API запрещены. Все запросы идут через клиент. + +Исключение допускается точечно — например, разовая отладочная проверка эндпоинта в скрипте — и требует обоснования в коде (комментарий с причиной). + +## Использование + +```ts +import { petProjectApi } from 'infrastructure/pet-project-api' + +const post = await petProjectApi.posts.get('my-post') +const list = await petProjectApi.posts.filter({ limit: 10, categories: [1, 2] }) +const form = await petProjectApi.forms.get('contact') +``` + +Стиль вызовов совпадает с автогенерированным клиентом — потребитель не различает, ручной API или сгенерирован. diff --git a/docs/docs/usage/data/rest/fetching/client.md b/docs/docs/usage/data/rest/fetching/client.md new file mode 100644 index 0000000..501eb2b --- /dev/null +++ b/docs/docs/usage/data/rest/fetching/client.md @@ -0,0 +1,165 @@ +--- +title: Клиентские компоненты +keywords: [swr, клиентские компоненты, useSWR, хук, мутация, useSWRMutation, кеш, ревалидация] +--- + +# Клиентские компоненты + +В клиентских компонентах данные получаются через **готовые хуки**, которые экспортируются из модуля API. SWR инкапсулирован в хуке — компонент не знает про `useSWR`, ключи и fetcher. + +Создание клиента и хуков — [Автоматическая](/docs/usage/data/rest/clients/auto) / [Ручная](/docs/usage/data/rest/clients/manual) генерация. + +## Правила + +- **Только готовые хуки.** В компоненте — `usePostDetail(slug)`, не `useSWR(['post', slug], () => api.posts.get(slug))`. +- **`useSWR` пишется один раз — в `hooks/`** модуля API. В клиентских компонентах никогда напрямую. +- **Прямой вызов методов клиента в `useEffect` запрещён.** Это потеря кеша, повторные запросы и гонки. +- **Мутации — через `useSWRMutation`**, тоже инкапсулированный в хуке. В компоненте вызывается готовый `trigger`. + +## Чтение + +```tsx +'use client' + +import { usePostDetail } from 'infrastructure/pet-project-api' + +export function PostView({ slug }: { slug: string }) { + const { data: post, error, isLoading } = usePostDetail(slug) + + if (isLoading) return + if (error) return + + return
{post?.title}
+} +``` + +В компоненте нет `useSWR`, нет ключей, нет fetcher — только готовый хук. + +## Параметризованный запрос + +Хук сам обрабатывает «нет параметра — нет запроса». В компоненте можно безопасно передавать `null`: + +```tsx +'use client' + +import { useUserDetail } from 'infrastructure/pet-project-api' + +export function UserProfile({ userId }: { userId: string | null }) { + const { data: user } = useUserDetail(userId) + + if (!userId) return + return +} +``` + +Внутри `useUserDetail` ключ становится `null`, когда `userId` не задан, и SWR не делает запрос — это поведение зашито в хук, потребитель об этом не думает. + +## Мутации + +Мутации тоже оборачиваются в хук модуля API: + +```ts +// src/infrastructure/pet-project-api/hooks/use-create-user.hook.ts +import useSWRMutation from 'swr/mutation' +import { mutate } from 'swr' +import { petProjectApi } from '..' +import type { User, UserCreateInput } from '../types' + +/** + * Создание пользователя с инвалидацией списка. + */ +export const useCreateUser = () => { + return useSWRMutation( + ['pet-project-api', 'user', 'create'], + (_key, { arg }) => petProjectApi.user.create(arg), + { + onSuccess: () => mutate(['pet-project-api', 'user', 'list']), + }, + ) +} +``` + +```tsx +'use client' + +import { useCreateUser } from 'infrastructure/pet-project-api' + +export function CreateUserForm() { + const { trigger, isMutating } = useCreateUser() + + return ( + trigger(input)} + disabled={isMutating} + /> + ) +} +``` + +В компоненте — снова только хук. Логика инвалидации кеша зашита внутрь, потребитель её не дублирует. + +## Передача config из компонента + +Каждый хук принимает второй (или третий) параметр `config?: SWRConfiguration` — он пробрасывается в `useSWR`. Это даёт потребителю точечно настроить ревалидацию, `fallbackData`, `suspense` и т.п.: + +```tsx +'use client' + +import { usePostDetail } from 'infrastructure/pet-project-api' + +export function PostView({ slug, initialPost }: Props) { + const { data: post } = usePostDetail(slug, { fallbackData: initialPost }) + // ... +} +``` + +## Начальное состояние с сервера + +Если данные пришли из серверного компонента (см. [Серверные компоненты](/docs/usage/data/rest/fetching/server)) — передаются в `fallbackData` через `config` хука: + +```tsx +// page.tsx (server) +import { petProjectApi } from 'infrastructure/pet-project-api' + +export default async function Page({ params }: { params: { slug: string } }) { + const initialPost = await petProjectApi.posts.get(params.slug) + return +} +``` + +```tsx +// post-view.tsx ('use client') +import { usePostDetail } from 'infrastructure/pet-project-api' + +export function PostView({ slug, initialPost }: Props) { + const { data: post } = usePostDetail(slug, { fallbackData: initialPost }) + return
{post?.title}
+} +``` + +Для массового заполнения кеша на странице с несколькими хуками — используется `` обёртка. Серверный компонент собирает данные и передаёт сериализованную карту ключей в провайдер; все вложенные хуки сразу видят кеш. + +## Запрет прямых вызовов + +```tsx +// Плохо — прямой fetch в обход клиента +useEffect(() => { + fetch('/api/users').then(...) +}, []) + +// Плохо — клиент без SWR: нет кеша, нет дедупликации +useEffect(() => { + petProjectApi.user.list().then(setUsers) +}, []) + +// Плохо — useSWR в компоненте: SWR должен быть в хуке модуля +const { data } = useSWR( + ['pet-project-api', 'user', 'list'], + () => petProjectApi.user.list(), +) + +// Хорошо — готовый хук модуля +const { data } = useUserList() +``` + +Если для нужной операции хука ещё нет — он добавляется в `hooks/` модуля API, не в компонент. diff --git a/docs/docs/usage/data/rest/fetching/server.md b/docs/docs/usage/data/rest/fetching/server.md new file mode 100644 index 0000000..c9bb91c --- /dev/null +++ b/docs/docs/usage/data/rest/fetching/server.md @@ -0,0 +1,67 @@ +--- +title: Серверные компоненты +keywords: [server components, rsc, серверные компоненты, fetch, api, app router, прямой вызов] +--- + +# Серверные компоненты + +В серверных компонентах (Server Components App Router) данные получаются **прямым вызовом метода API-клиента**. SWR и хуки здесь не применяются — они для клиентского кода. + +Создание клиента — [Автоматическая](/docs/usage/data/rest/clients/auto) / [Ручная](/docs/usage/data/rest/clients/manual) генерация. + +## Правила + +- **Прямой `await` метода клиента.** Никаких хуков, обёрток состояний, `useEffect` — серверный компонент не имеет жизненного цикла React-клиента. +- **Ошибки бросаются.** Не оборачивать `try/catch` без необходимости — Next.js поднимет ближайший `error.tsx`. +- **Параллельные запросы — через `Promise.all`.** Последовательный `await` за `await` блокирует рендер. + +## Шаблон + +```tsx +// src/app/(routes)/users/page.tsx +import { petProjectApi } from 'infrastructure/pet-project-api' + +export default async function UsersPage() { + const users = await petProjectApi.user.list() + + return ( +
    + {users.map((user) => ( +
  • {user.name}
  • + ))} +
+ ) +} +``` + +## Параллельные запросы + +```tsx +export default async function DashboardPage() { + const [users, orders] = await Promise.all([ + petProjectApi.user.list(), + petProjectApi.order.list(), + ]) + + return +} +``` + +## Передача данных в клиентский компонент + +Серверный компонент получает данные и передаёт их пропсами в клиентский. На клиенте данные становятся начальным состоянием — при необходимости перезапрашиваются через SWR (см. [Клиентские компоненты](/docs/usage/data/rest/fetching/client)). + +```tsx +// page.tsx (server) +import { petProjectApi } from 'infrastructure/pet-project-api' +import { UsersList } from 'widgets/users-list' + +export default async function UsersPage() { + const initialUsers = await petProjectApi.user.list() + return +} +``` + +## Запрет прямого `fetch` + +Серверный компонент тоже использует только клиент из `infrastructure/`. Прямой `fetch` в `page.tsx` или в server-action запрещён теми же правилами, что и на клиенте. diff --git a/docs/docs/applied/fonts.md b/docs/docs/usage/fonts.md similarity index 100% rename from docs/docs/applied/fonts.md rename to docs/docs/usage/fonts.md diff --git a/docs/docs/applied/hooks.md b/docs/docs/usage/hooks.md similarity index 100% rename from docs/docs/applied/hooks.md rename to docs/docs/usage/hooks.md diff --git a/docs/docs/applied/images-sprites.md b/docs/docs/usage/images-sprites.md similarity index 100% rename from docs/docs/applied/images-sprites.md rename to docs/docs/usage/images-sprites.md diff --git a/docs/docs/applied/localization.md b/docs/docs/usage/localization.md similarity index 100% rename from docs/docs/applied/localization.md rename to docs/docs/usage/localization.md diff --git a/docs/docs/applied/page-level.md b/docs/docs/usage/page-level.md similarity index 100% rename from docs/docs/applied/page-level.md rename to docs/docs/usage/page-level.md diff --git a/docs/docs/applied/project-structure.md b/docs/docs/usage/project-structure.md similarity index 100% rename from docs/docs/applied/project-structure.md rename to docs/docs/usage/project-structure.md diff --git a/docs/docs/applied/stores.md b/docs/docs/usage/stores.md similarity index 100% rename from docs/docs/applied/stores.md rename to docs/docs/usage/stores.md diff --git a/docs/docs/applied/styles/usage.md b/docs/docs/usage/styles.md similarity index 98% rename from docs/docs/applied/styles/usage.md rename to docs/docs/usage/styles.md index 4169cf8..039e7d5 100644 --- a/docs/docs/applied/styles/usage.md +++ b/docs/docs/usage/styles.md @@ -4,7 +4,7 @@ title: Использование # Использование -Правила написания CSS: PostCSS Modules, форматирование, переменные. Установка и настройка процессора — [PostCSS](/docs/applied/styles/postcss). +Правила написания CSS: PostCSS Modules, форматирование, переменные. Установка и настройка процессора — [PostCSS](/docs/setup/postcss). ## Общие правила diff --git a/docs/docs/applied/svg-sprites/usage.md b/docs/docs/usage/svg-sprites.md similarity index 98% rename from docs/docs/applied/svg-sprites/usage.md rename to docs/docs/usage/svg-sprites.md index c8aa94e..28f8d4c 100644 --- a/docs/docs/applied/svg-sprites/usage.md +++ b/docs/docs/usage/svg-sprites.md @@ -5,7 +5,7 @@ keywords: [svg, спрайт, sprite, иконка, icon, SvgSprite, превь # Использование -Работа с SVG-иконками через сгенерированный компонент ``. Установка пакета — [Установка и настройка](/docs/applied/svg-sprites/setup). +Работа с SVG-иконками через сгенерированный компонент ``. Установка пакета — [Установка и настройка](/docs/setup/svg-sprites). ## Шаги diff --git a/docs/docs/applied/templates-generation.md b/docs/docs/usage/templates-generation.md similarity index 100% rename from docs/docs/applied/templates-generation.md rename to docs/docs/usage/templates-generation.md diff --git a/docs/docs/applied/video.md b/docs/docs/usage/video.md similarity index 100% rename from docs/docs/applied/video.md rename to docs/docs/usage/video.md diff --git a/docs/docs/workflow/code-generation.md b/docs/docs/workflow/code-generation.md index a74a5ed..bc335b8 100644 --- a/docs/docs/workflow/code-generation.md +++ b/docs/docs/workflow/code-generation.md @@ -28,4 +28,4 @@ title: Генерация кода - Повторяющаяся структура появляется больше одного раза. - Существующий шаблон не покрывает нужный тип модуля. -Инструменты и синтаксис шаблонов — [Шаблоны и генерация кода](/docs/applied/templates-generation). +Инструменты и синтаксис шаблонов — [Шаблоны и генерация кода](/docs/usage/templates-generation). diff --git a/docs/docs/workflow/creating-components.md b/docs/docs/workflow/creating-components.md index 126b9a4..cb67073 100644 --- a/docs/docs/workflow/creating-components.md +++ b/docs/docs/workflow/creating-components.md @@ -12,11 +12,11 @@ title: Добавление UI-модуля ## Порядок действий -1. [Сгенерировать](/docs/applied/templates-generation) модуль из соответствующего шаблона в целевой слой. +1. [Сгенерировать](/docs/usage/templates-generation) модуль из соответствующего шаблона в целевой слой. 2. Заполнить модуль логикой и стилями. ## Дочерние компоненты -Если модулю нужны внутренние подкомпоненты — [генерировать](/docs/applied/templates-generation) их из шаблона `component` в папку `ui/` внутри родительского модуля. Дочерние компоненты не экспортируются через `index.ts` родителя. +Если модулю нужны внутренние подкомпоненты — [генерировать](/docs/usage/templates-generation) их из шаблона `component` в папку `ui/` внутри родительского модуля. Дочерние компоненты не экспортируются через `index.ts` родителя. -Правила написания компонентов — [Компоненты](/docs/applied/components). +Правила написания компонентов — [Компоненты](/docs/usage/components). diff --git a/docs/docs/workflow/creating-pages.md b/docs/docs/workflow/creating-pages.md index b9c3785..e902063 100644 --- a/docs/docs/workflow/creating-pages.md +++ b/docs/docs/workflow/creating-pages.md @@ -12,7 +12,7 @@ title: Добавление страницы ## Порядок действий -1. [Сгенерировать](/docs/applied/templates-generation) экран из шаблона `screen` в папку `src/screens/`. +1. [Сгенерировать](/docs/usage/templates-generation) экран из шаблона `screen` в папку `src/screens/`. 2. Заполнить экран логикой и стилями. @@ -20,8 +20,8 @@ title: Добавление страницы ## Правила -- Ручное создание файловой структуры экрана запрещено — только [генерация](/docs/applied/templates-generation) из шаблона. +- Ручное создание файловой структуры экрана запрещено — только [генерация](/docs/usage/templates-generation) из шаблона. - Логика, стили и зависимости размещаются в экране, не в `page.tsx`. - Каждая страница содержит `metadata` с `title` и `description`. -Примеры `page.tsx` и `metadata` — [Page-level компоненты](/docs/applied/page-level). +Примеры `page.tsx` и `metadata` — [Page-level компоненты](/docs/usage/page-level). diff --git a/docs/docs/workflow/getting-started.md b/docs/docs/workflow/getting-started.md index 630b879..9911d71 100644 --- a/docs/docs/workflow/getting-started.md +++ b/docs/docs/workflow/getting-started.md @@ -19,4 +19,4 @@ title: Начало работы ## Настройка окружения -Открыть проект в VS Code и установить рекомендуемые расширения — редактор предложит это автоматически. Подробнее — [Настройка VS Code](/docs/applied/vscode). +Открыть проект в VS Code и установить рекомендуемые расширения — редактор предложит это автоматически. Подробнее — [Настройка VS Code](/docs/setup/vscode). diff --git a/docs/docs/workflow/styling.md b/docs/docs/workflow/styling.md index 966d9a4..5f6491f 100644 --- a/docs/docs/workflow/styling.md +++ b/docs/docs/workflow/styling.md @@ -20,4 +20,4 @@ title: Стилизация - **Магические значения** — произвольные цвета, отступы и скругления запрещены, использовать токены. - **Глобальные стили** вне `app/styles/` запрещены. -Правила написания CSS, вложенность, медиа-запросы и токены — [Стили: использование](/docs/applied/styles/usage). +Правила написания CSS, вложенность, медиа-запросы и токены — [Стили: использование](/docs/usage/styles).