docs: обновить структуру прикладных разделов
All checks were successful
CI/CD Pipeline / docker (push) Successful in 51s
CI/CD Pipeline / deploy (push) Successful in 7s

- перенесён раздел создания проекта в прикладные разделы

- переработаны разделы REST-клиента и получения данных

- удалена устаревшая категория работы с данными

- обновлён стандартный конфиг Biome

- добавлены правила для обязательного API baseUrl без fallback

- обновлены сайдбар, карта документации, README и ссылки
This commit is contained in:
2026-05-08 19:12:26 +03:00
parent eadc4627f7
commit 8231356d8d
27 changed files with 737 additions and 666 deletions

View File

@@ -29,59 +29,53 @@ const sidebar = [
{ text: 'Типизация', link: '/docs/basics/typing' }, { text: 'Типизация', link: '/docs/basics/typing' },
], ],
}, },
{
text: 'Создание проекта',
items: [
{ text: 'Из шаблона', link: '/docs/creating-project/from-template' },
{ text: 'По гайду вручную', link: '/docs/creating-project/manual' },
{ text: 'Чистый Next.js', link: '/docs/creating-project/nextjs' },
],
},
{
text: 'Работа с данными',
// collapsed: true,
items: [
{ text: 'Введение', link: '/docs/data/' },
{
text: 'REST',
collapsed: true,
items: [
{ text: 'Обзор', link: '/docs/data/rest/' },
{
text: 'Создание клиента',
collapsed: true,
items: [
{ text: 'Обзор', link: '/docs/data/rest/clients/' },
{ text: 'Автогенерация из OpenAPI', link: '/docs/data/rest/clients/auto' },
{ text: 'Ручное создание', link: '/docs/data/rest/clients/manual' },
{ text: 'GET-хуки REST-клиента', link: '/docs/data/rest/clients/hooks' },
],
},
{
text: 'Использование',
collapsed: true,
items: [
{ text: 'Стратегии получения данных', link: '/docs/data/rest/strategies/' },
{ text: 'Серверный await', link: '/docs/data/rest/strategies/server-await' },
{ text: 'Параллельные серверные запросы', link: '/docs/data/rest/strategies/parallel-server-requests' },
{ text: 'Передача промиса ниже', link: '/docs/data/rest/strategies/pass-promise-down' },
{ text: 'Начальные данные для клиентских хуков', link: '/docs/data/rest/strategies/client-hooks-initial-data' },
{ text: 'Клиентский GET-хук', link: '/docs/data/rest/strategies/client-get-hook' },
{ text: 'Business-композиция', link: '/docs/data/rest/strategies/business-composition' },
],
},
],
},
{ text: 'Realtime', link: '/docs/data/realtime' },
],
},
{ {
text: 'Прикладные разделы', text: 'Прикладные разделы',
items: [ items: [
{
text: 'Создание проекта',
collapsed: true,
items: [
{ text: 'Из шаблона', link: '/docs/applied/creating-project/from-template' },
{ text: 'По гайду вручную', link: '/docs/applied/creating-project/manual' },
{ text: 'Чистый Next.js', link: '/docs/applied/creating-project/nextjs' },
],
},
{ text: 'Структура проекта', link: '/docs/applied/project-structure' }, { text: 'Структура проекта', link: '/docs/applied/project-structure' },
{ text: 'Страницы', link: '/docs/applied/page-level' }, { text: 'Страницы', link: '/docs/applied/page-level' },
{ text: 'Компонент', link: '/docs/applied/component' }, { text: 'Компонент', link: '/docs/applied/component' },
{ text: 'Модуль', link: '/docs/applied/module' }, { text: 'Модуль', link: '/docs/applied/module' },
{
text: 'REST-клиент',
collapsed: true,
items: [
{ text: 'Введение', link: '/docs/applied/rest-client/' },
{
text: 'Настройка',
collapsed: true,
items: [
{ text: 'Обзор', link: '/docs/applied/rest-client/setup/' },
{ text: 'Автогенерация из OpenAPI', link: '/docs/applied/rest-client/setup/auto' },
{ text: 'Ручное создание', link: '/docs/applied/rest-client/setup/manual' },
{ text: 'GET-хуки REST-клиента', link: '/docs/applied/rest-client/setup/hooks' },
],
},
{ text: 'Использование', link: '/docs/applied/rest-client/usage' },
],
},
{
text: 'Получение данных',
collapsed: true,
items: [
{ text: 'Обзор', link: '/docs/applied/data-fetch/' },
{ text: 'Серверный await', link: '/docs/applied/data-fetch/server-await' },
{ text: 'Параллельные серверные запросы', link: '/docs/applied/data-fetch/parallel-server-requests' },
{ text: 'Передача промиса ниже', link: '/docs/applied/data-fetch/pass-promise-down' },
{ text: 'Начальные данные для клиентских хуков', link: '/docs/applied/data-fetch/client-hooks-initial-data' },
{ text: 'Клиентский GET-хук', link: '/docs/applied/data-fetch/client-get-hook' },
{ text: 'Business-композиция', link: '/docs/applied/data-fetch/business-composition' },
],
},
{ {
text: 'Стили', text: 'Стили',
collapsed: true, collapsed: true,

View File

@@ -33,19 +33,23 @@ docs/
│ ├── naming.md │ ├── naming.md
│ ├── documentation.md │ ├── documentation.md
│ └── typing.md │ └── typing.md
└── applied/ # Прикладные разделы: настройка и использование
├── creating-project/ # Создание проекта: как поднять новый проект ├── creating-project/ # Создание проекта: как поднять новый проект
│ ├── from-template.md │ ├── from-template.md
│ ├── manual.md │ ├── manual.md
│ └── nextjs.md │ └── nextjs.md
├── data/ # Работа с данными
│ ├── index.md
│ ├── realtime.md
│ └── rest/
└── applied/ # Прикладные разделы: настройка и использование
├── project-structure.md ├── project-structure.md
├── page-level.md ├── page-level.md
├── component.md ├── component.md
├── module.md ├── module.md
├── rest-client/ # REST-клиент: настройка клиента сервиса
│ ├── index.md
│ ├── usage.md
│ └── setup/
├── data-fetch/ # Получение данных: стратегии серверного и клиентского получения
│ ├── index.md
│ ├── server-await.md
│ └── client-get-hook.md
├── styles/ # Стили: настройка + использование ├── styles/ # Стили: настройка + использование
│ ├── styles-setup.md │ ├── styles-setup.md
│ └── styles-usage.md │ └── styles-usage.md
@@ -76,15 +80,14 @@ generate-llms.ts # Скрипт генерации llms.txt и R
### Добавление нового раздела ### Добавление нового раздела
1. Создать `.md`-файл в нужной папке: `basics/`, `creating-project/`, 1. Создать `.md`-файл в нужной папке: `basics/` или `applied/`.
или `applied/`.
2. Добавить пункт в сайдбар — `.vitepress/config.ts`. 2. Добавить пункт в сайдбар — `.vitepress/config.ts`.
Сайдбар — единственный источник порядка и группировки для `llms.txt`. Сайдбар — единственный источник порядка и группировки для `llms.txt`.
3. Запустить `npm run llms` для обновления `llms.txt` и README. 3. Запустить `npm run llms` для обновления `llms.txt` и README.
## Типы разделов ## Типы разделов
Документация разделена на четыре группы. Каждая отвечает на свой вопрос Документация разделена на две основные группы. Каждая отвечает на свой вопрос
и имеет свою природу — это влияет на содержимое и структуру страницы. и имеет свою природу — это влияет на содержимое и структуру страницы.
### Базовые правила (`basics/`) ### Базовые правила (`basics/`)
@@ -101,30 +104,25 @@ generate-llms.ts # Скрипт генерации llms.txt и R
**Граница:** если правило касается только одной области (только стили, **Граница:** если правило касается только одной области (только стили,
только компоненты, только API) — оно живёт в прикладном разделе, не в базовых. только компоненты, только API) — оно живёт в прикладном разделе, не в базовых.
### Создание проекта (`creating-project/`)
**Отвечает на вопрос:** «Как поднять новый проект?»
Сценарии запуска нового проекта целиком: из шаблона, вручную, чистая
установка фреймворка. Раздел описывает порядок шагов на уровне всего
проекта; детали отдельных инструментов лежат в `applied/`.
**Граница:** не дублирует разделы `applied/`. Ссылается на них как на
шаги в общем сценарии.
### Прикладные разделы (`applied/`) ### Прикладные разделы (`applied/`)
**Отвечает на вопрос:** «Как поставить инструмент и как им пользоваться **Отвечает на вопрос:** «Как решить практическую задачу в проекте
Прикладные разделы объединяют настройку и использование инструментов Прикладные разделы объединяют создание проекта, настройку и использование инструментов
и подсистем. Каждый раздел — самостоятельная предметная область. и подсистем. Каждый раздел — самостоятельная предметная область.
Разделы делятся на два типа: `applied/creating-project/` отвечает на вопрос «Как поднять новый проект?» и описывает сценарии запуска нового проекта целиком: из шаблона, вручную, чистую установку фреймворка. Детали отдельных инструментов остаются в собственных прикладных разделах, а создание проекта ссылается на них как на шаги общего сценария.
1. **Только настройка** — разовая установка инструмента (линтер, Разделы делятся на три типа:
1. **Создание проекта** — сценарии запуска проекта целиком. Живут в
`applied/creating-project/` и в сайдбаре оформляются как первая группа
прикладных разделов.
2. **Только настройка** — разовая установка инструмента (линтер,
CSS-процессор, алиасы). Файл без суффикса: `biome.md`, `postcss.md`. CSS-процессор, алиасы). Файл без суффикса: `biome.md`, `postcss.md`.
2. **Настройка + использование** — область, требующая и установки, 3. **Настройка + использование** — область, требующая и установки,
и повседневных правил. Два файла с суффиксами: `styles-setup.md` и повседневных правил. Два файла с суффиксами: `styles-setup.md`
(настройка) и `styles-usage.md` (использование). В сайдбаре (настройка) и `styles-usage.md` (использование). В сайдбаре
оборачиваются в collapsed-группу. оборачиваются в collapsed-группу.
@@ -136,7 +134,7 @@ generate-llms.ts # Скрипт генерации llms.txt и R
## Структура прикладного раздела ## Структура прикладного раздела
Шаблон ниже относится к usage-страницам прикладных разделов (`applied/*-usage.md`). Шаблон ниже относится к usage-страницам прикладных разделов (`applied/*-usage.md`).
Setup-страницы (`applied/*-setup.md`) и `creating-project/` имеют другую Setup-страницы (`applied/*-setup.md`) и `applied/creating-project/` имеют другую
структуру — ориентированную на пошаговую установку (требования → установка → структуру — ориентированную на пошаговую установку (требования → установка →
проверка). проверка).
@@ -260,7 +258,7 @@ description: Описание раздела одним предложением
- Исключение: имя инструмента допустимо, если оно — единственное - Исключение: имя инструмента допустимо, если оно — единственное
устойчивое имя самой области (`PostCSS`, `Biome`, `VS Code`). устойчивое имя самой области (`PostCSS`, `Biome`, `VS Code`).
- Если страница вложена в семантическую группу - Если страница вложена в семантическую группу
(`Архитектура → Слои`, `Данные → REST → Серверные компоненты`) (`Архитектура → Слои`, `Прикладные → REST-клиент → Использование`)
и короткое имя теряет смысл при прямой ссылке — `h1` поднимает и короткое имя теряет смысл при прямой ссылке — `h1` поднимает
имя родителя в заголовок: `Слои SLM`, `Сегменты SLM`. В сайдбаре имя родителя в заголовок: `Слои SLM`, `Сегменты SLM`. В сайдбаре
допустимо оставить короткий вариант (`Слои`, `Сегменты`) — там допустимо оставить короткий вариант (`Слои`, `Сегменты`) — там
@@ -309,7 +307,7 @@ description: Описание раздела одним предложением
- Звучит как ответ человека другу, а не как техспек. - Звучит как ответ человека другу, а не как техспек.
- Описание читается **самостоятельно**, без контекста сайдбара. - Описание читается **самостоятельно**, без контекста сайдбара.
- Если страница вложена в семантическую группу - Если страница вложена в семантическую группу
(например, `Данные → RESTКлиенты → ...`) и её заголовок (например, `Прикладные → RESTлиент → Настройка → ...`) и её заголовок
без этой группы теряет смысл — описание явно содержит имя без этой группы теряет смысл — описание явно содержит имя
родительской области, чтобы читалось без сайдбара. родительской области, чтобы читалось без сайдбара.

View File

@@ -37,22 +37,15 @@
| Документирование | Как писать JSDoc: что документировать, а что нет? | | Документирование | Как писать JSDoc: что документировать, а что нет? |
| Типизация | Как типизировать: type vs interface, any/unknown? | | Типизация | Как типизировать: type vs interface, any/unknown? |
### Создание проекта
**Как начать новый проект** — варианты установки и эталонный набор инструментов.
| Раздел | Отвечает на вопрос |
|--------|-------------------|
| Создание проекта из шаблона | Как начать проект из готового шаблона? |
| Создание проекта вручную | Как поднять проект с нуля без шаблона? |
| Чистая установка Next.js | Как поставить голый Next.js под дальнейшую сборку? |
### Настройка ### Настройка
**Как сконфигурировать проект** — пошаговая настройка инструментов и инфраструктуры. **Как сконфигурировать проект** — пошаговая настройка инструментов и инфраструктуры.
| Раздел | Отвечает на вопрос | | Раздел | Отвечает на вопрос |
|--------|-------------------| |--------|-------------------|
| Создание проекта из шаблона | Как начать проект из готового шаблона? |
| Создание проекта вручную | Как поднять проект с нуля без шаблона? |
| Чистая установка Next.js | Как поставить голый Next.js под дальнейшую сборку? |
| Алиасы импортов | Как настроить алиасы импортов? | | Алиасы импортов | Как настроить алиасы импортов? |
| Biome | Как настроить линтер и форматтер? | | Biome | Как настроить линтер и форматтер? |
| PostCSS | Какие плагины PostCSS нужны и как их настроить? | | PostCSS | Какие плагины PostCSS нужны и как их настроить? |
@@ -70,6 +63,8 @@
| Структура проекта | Как организованы папки и файлы по SLM? | | Структура проекта | Как организованы папки и файлы по SLM? |
| Компоненты | Как устроен компонент: файлы, пропсы, clsx? | | Компоненты | Как устроен компонент: файлы, пропсы, clsx? |
| Файлы роутинга | Как описывать layout, page, loading, error, not-found? | | Файлы роутинга | Как описывать layout, page, loading, error, not-found? |
| REST-клиент | Как настроить клиент внешнего REST API? |
| Получение данных | Как выбрать способ получения данных под рендер страницы? |
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? | | Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
| Стили | Как писать CSS: вложенность, медиа, токены? | | Стили | Как писать CSS: вложенность, медиа, токены? |
| SVG-спрайты | Как использовать SVG-спрайты в коде? | | SVG-спрайты | Как использовать SVG-спрайты в коде? |
@@ -79,16 +74,3 @@
| Хуки | _(не заполнен)_ | | Хуки | _(не заполнен)_ |
| Шрифты | _(не заполнен)_ | | Шрифты | _(не заполнен)_ |
| Локализация | _(не заполнен)_ | | Локализация | _(не заполнен)_ |
### Данные
**Как работать с источниками данных** — REST, realtime и потребление в компонентах.
| Раздел | Отвечает на вопрос |
|--------|-------------------|
| Источники данных | Как устроена работа с данными в проекте? |
| REST: Автоматическая генерация | Как сгенерировать REST-клиент автоматически из OpenAPI? |
| REST: Ручное создание | Как написать REST-клиент вручную? |
| REST: Серверные компоненты | Как получать данные в серверных компонентах? |
| REST: Клиентские компоненты | Как получать данные в клиентских компонентах? |
| Realtime | Как работать с realtime-каналами и сокетами? |

View File

@@ -19,35 +19,28 @@
- [Документирование](./basics/documentation.md) — Что и как документировать в коде. - [Документирование](./basics/documentation.md) — Что и как документировать в коде.
- [Типизация](./basics/typing.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/project-structure.md) — Из чего состоит проект и где что лежит.
- [Страницы](./applied/page-level.md) — Как работать со страницами и другими файлами роутинга Next.js App Router. - [Страницы](./applied/page-level.md) — Как работать со страницами и другими файлами роутинга Next.js App Router.
- [Компонент](./applied/component.md) — Как создавать React-компоненты внутри SLM-модулей. - [Компонент](./applied/component.md) — Как создавать React-компоненты внутри SLM-модулей.
- [Модуль](./applied/module.md) — Как создавать и организовывать 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-setup.md) — Подготовка стилевой основы проекта: токены, медиа-запросы, глобальные стили.
- [Стили: Использование](./applied/styles/styles-usage.md) — Как пишутся стили в проекте. - [Стили: Использование](./applied/styles/styles-usage.md) — Как пишутся стили в проекте.
- [SVG-спрайты](./applied/svg-sprites/svg-sprites-intro.md) — Что такое SVG-спрайты и какие проблемы они решают. - [SVG-спрайты](./applied/svg-sprites/svg-sprites-intro.md) — Что такое SVG-спрайты и какие проблемы они решают.

View File

@@ -29,7 +29,7 @@ keywords: [biome, линтер, форматтер, lint, format, biome.json, "@
В корне появится `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`: 4. Добавить скрипты в `package.json`:
@@ -51,30 +51,63 @@ keywords: [biome, линтер, форматтер, lint, format, biome.json, "@
## Стандартный `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 ```jsonc
{ {
"overrides": [ "$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
{ "vcs": {
"includes": ["**/*.css"], "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": { "linter": {
"enabled": true,
"rules": { "rules": {
"recommended": true,
"suspicious": { "suspicious": {
"noUnknownAtRules": "off" "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 ## Интеграция с VS Code

View File

@@ -14,11 +14,11 @@ keywords: [создать проект из шаблона, шаблон, templa
- **Стек:** Next.js (App Router), TypeScript, React. - **Стек:** Next.js (App Router), TypeScript, React.
- **Архитектура:** структура папок по SLM, алиасы импортов. - **Архитектура:** структура папок по SLM, алиасы импортов.
- **Качество кода:** Biome (линтер и форматер), настройки VS Code. - **Качество кода:** Biome (линтер и форматтер), настройки VS Code.
- **Стили:** PostCSS Modules с плагинами, токены, медиа-брейкпоинты. - **Стили:** PostCSS Modules с плагинами, токены, медиа-брейкпоинты.
- **Ассеты:** генерация SVG-спрайтов. - **Ассеты:** генерация SVG-спрайтов.
- **Кодогенерация:** шаблоны для страниц, компонентов, хуков, сторов. - **Кодогенерация:** шаблоны для страниц, компонентов, хуков, сторов.
в
## Установка ## Установка
1. Склонировать шаблон в родительском каталоге будущего проекта: 1. Склонировать шаблон в родительском каталоге будущего проекта:

View File

@@ -12,7 +12,7 @@ keywords: [создать проект, новый проект, с нуля, in
| Компонент | Роль | Раздел | | Компонент | Роль | Раздел |
|-----------|------|--------| |-----------|------|--------|
| Next.js | Фреймворк (роутинг, сборка, SSR) | [Next.js](/docs/creating-project/nextjs) | | Next.js | Фреймворк (роутинг, сборка, SSR) | [Next.js](/docs/applied/creating-project/nextjs) |
| Алиасы | Импорты по слоям SLM | [Алиасы](/docs/applied/aliases) | | Алиасы | Импорты по слоям SLM | [Алиасы](/docs/applied/aliases) |
| Biome | Линтер и форматтер (замена ESLint + Prettier) | [Biome](/docs/applied/biome) | | Biome | Линтер и форматтер (замена ESLint + Prettier) | [Biome](/docs/applied/biome) |
| Стили | Глобальные токены и breakpoints | [Стили](/docs/applied/styles/styles-setup) | | Стили | Глобальные токены и breakpoints | [Стили](/docs/applied/styles/styles-setup) |
@@ -37,7 +37,7 @@ keywords: [создать проект, новый проект, с нуля, in
Скелет фреймворка — обязательный первый шаг, остальное опирается на него. Скелет фреймворка — обязательный первый шаг, остальное опирается на него.
См. [Next.js](/docs/creating-project/nextjs). После выполнения проверки этого раздела `npm run build` должен проходить. См. [Next.js](/docs/applied/creating-project/nextjs). После выполнения проверки этого раздела `npm run build` должен проходить.
### 2. Алиасы ### 2. Алиасы

View File

@@ -21,13 +21,13 @@ Business-композиция используется, когда просто
```ts ```ts
// src/business/pets/hooks/use-available-pets.hook.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 = () => { export const useAvailablePets = () => {
const query = useGetPetList('available') const query = useGetPetList({ status: StatusEnum.Available })
return { return {
...query, ...query,
@@ -42,15 +42,15 @@ export const useAvailablePets = () => {
```ts ```ts
// src/business/pets/hooks/use-pets-dashboard.hook.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 по питомцам. * Данные dashboard по питомцам.
*/ */
export const usePetsDashboard = () => { export const usePetsDashboard = () => {
const availablePets = useGetPetList('available') const availablePets = useGetPetList({ status: StatusEnum.Available })
const pendingPets = useGetPetList('pending') const pendingPets = useGetPetList({ status: StatusEnum.Pending })
const soldPets = useGetPetList('sold') const soldPets = useGetPetList({ status: StatusEnum.Sold })
return { return {
availablePets, availablePets,
@@ -108,7 +108,7 @@ src/business/
```ts ```ts
// Плохо — business-смысл внутри infra-хука // Плохо — business-смысл внутри infra-хука
export const useGetPetList = (status: PetStatus) => { export const useGetPetList = (params?: FindPetsByStatusParams | null) => {
const query = useSWR(...) const query = useSWR(...)
return { return {

View File

@@ -21,14 +21,13 @@ keywords: [rest, client components, swr, get-хук, client state]
'use client' 'use client'
import { useState } from 'react' import { useState } from 'react'
import { useGetPetList } from 'infra/pet-store-api' import { StatusEnum, useGetPetList } from 'infra/pet-store-api'
import type { PetStatus } from 'infra/pet-store-api'
const statuses: PetStatus[] = ['available', 'pending', 'sold'] const statuses = [StatusEnum.Available, StatusEnum.Pending, StatusEnum.Sold]
export function PetTabs() { export function PetTabs() {
const [status, setStatus] = useState<PetStatus>('available') const [status, setStatus] = useState(StatusEnum.Available)
const { data: pets, isLoading, error } = useGetPetList(status) const { data: pets, isLoading, error } = useGetPetList({ status })
return ( return (
<section> <section>
@@ -75,7 +74,7 @@ useEffect(() => {
// Плохо — useSWR в компоненте // Плохо — useSWR в компоненте
const { data } = useSWR( const { data } = useSWR(
['pet-store-api', 'pet', 'list', status], ['pet-store-api', `/pet/findByStatus?status=${status}`],
() => petStoreApi.pet.findPetsByStatus({ status }), () => petStoreApi.pet.findPetsByStatus({ status }),
) )
``` ```
@@ -84,6 +83,6 @@ const { data } = useSWR(
## Когда выбрать другую стратегию ## Когда выбрать другую стратегию
- Данные нужны до первого HTML — [Серверный await](/docs/data/rest/strategies/server-await). - Данные нужны до первого HTML — [Серверный await](/docs/applied/data-fetch/server-await).
- Клиентский хук должен получить начальные данные сразу — [Начальные данные для клиентских хуков](/docs/data/rest/strategies/client-hooks-initial-data). - Клиентский хук должен получить начальные данные сразу — [Начальные данные для клиентских хуков](/docs/applied/data-fetch/client-hooks-initial-data).
- Нужно вычислить бизнес-состояние — [Business-композиция](/docs/data/rest/strategies/business-composition). - Нужно вычислить бизнес-состояние — [Business-композиция](/docs/applied/data-fetch/business-composition).

View File

@@ -31,8 +31,13 @@ keywords: [rest, swr, fallback, initial data, client hooks, unstable_serialize,
```ts ```ts
// src/infra/pet-store-api/hooks/use-get-pet-list.hook.ts // src/infra/pet-store-api/hooks/use-get-pet-list.hook.ts
export const getPetListKey = (status: PetStatus) => export const getPetListKey = (params?: FindPetsByStatusParams | null) => {
['pet-store-api', 'pet', 'list', status] as const if (!params?.status) {
return null
}
return ['pet-store-api', `/pet/findByStatus?status=${params.status}`] as const
}
``` ```
Ключ экспортируется из REST-модуля, потому что он нужен и GET-хуку, и серверному `SWRConfig fallback`. Ключ экспортируется из REST-модуля, потому что он нужен и GET-хуку, и серверному `SWRConfig fallback`.
@@ -46,6 +51,7 @@ import { SWRConfig, unstable_serialize } from 'swr'
import { import {
getPetListKey, getPetListKey,
petStoreApi, petStoreApi,
StatusEnum,
} from 'infra/pet-store-api' } from 'infra/pet-store-api'
type PetsLayoutProps = { type PetsLayoutProps = {
@@ -53,15 +59,14 @@ type PetsLayoutProps = {
} }
export default async function PetsLayout({ children }: PetsLayoutProps) { export default async function PetsLayout({ children }: PetsLayoutProps) {
const availablePetsPromise = petStoreApi.pet.findPetsByStatus({ const params = { status: StatusEnum.Available }
status: 'available', const availablePetsPromise = petStoreApi.pet.findPetsByStatus(params)
})
return ( return (
<SWRConfig <SWRConfig
value={{ value={{
fallback: { fallback: {
[unstable_serialize(getPetListKey('available'))]: availablePetsPromise, [unstable_serialize(getPetListKey(params))]: availablePetsPromise,
}, },
}} }}
> >
@@ -78,10 +83,12 @@ export default async function PetsLayout({ children }: PetsLayoutProps) {
```tsx ```tsx
'use client' 'use client'
import { useGetPetList } from 'infra/pet-store-api' import { StatusEnum, useGetPetList } from 'infra/pet-store-api'
export function PetList() { export function PetList() {
const { data: pets, isLoading } = useGetPetList('available') const { data: pets, isLoading } = useGetPetList({
status: StatusEnum.Available,
})
if (isLoading) return <div>Загрузка...</div> if (isLoading) return <div>Загрузка...</div>
@@ -100,10 +107,11 @@ export function PetList() {
## Что важно ## Что важно
- Ключ `fallback` должен совпадать с ключом GET-хука. - Ключ `fallback` должен совпадать с ключом GET-хука.
- `fallback` использует ту же key-функцию и те же params, что и GET-хук.
- Серверный код вызывает метод клиента, а не GET-хук. - Серверный код вызывает метод клиента, а не GET-хук.
- Клиентский компонент вызывает GET-хук, а не `useSWR` напрямую. - Клиентский компонент вызывает GET-хук, а не `useSWR` напрямую.
- Эта стратегия не означает ручную работу с кешем в компонентах. - Эта стратегия не означает ручную работу с кешем в компонентах.
## Когда не использовать ## Когда не использовать
Если данные нужны только серверному компоненту, используйте [Серверный await](/docs/data/rest/strategies/server-await). Если данные зависят от состояния браузера, используйте [Клиентский GET-хук](/docs/data/rest/strategies/client-get-hook). Если данные нужны только серверному компоненту, используйте [Серверный await](/docs/applied/data-fetch/server-await). Если данные зависят от состояния браузера, используйте [Клиентский GET-хук](/docs/applied/data-fetch/client-get-hook).

View File

@@ -1,14 +1,14 @@
--- ---
title: Стратегии получения данных title: Получение данных
description: Как выбрать получение REST-данных с учётом рендера страницы. description: Как получать данные с учётом рендера страницы.
keywords: [rest, стратегии, render, isr, ssr, server components, swr, promise, business] keywords: [rest, стратегии, render, isr, ssr, server components, swr, promise, business]
--- ---
# Стратегии получения данных # Получение данных
Как выбрать получение REST-данных с учётом рендера страницы. Как получать данные с учётом рендера страницы.
Перед выбором стратегии должен быть создан REST-клиент сервиса. Если клиента ещё нет, начните с раздела [Создание клиента](/docs/data/rest/clients/). Перед выбором стратегии должен быть настроен REST-клиент сервиса. Если клиента ещё нет, начните с раздела [Настройка REST-клиента](/docs/applied/rest-client/setup/).
## Сначала определите рендер страницы ## Сначала определите рендер страницы
@@ -52,12 +52,12 @@ SSR/dynamic rendering выбирается только когда данные
| Ситуация после выбора рендера | Стратегия | Где читать | | Ситуация после выбора рендера | Стратегия | Где читать |
|-------------------------------|-----------|------------| |-------------------------------|-----------|------------|
| Данные обязательны для первого HTML, SEO, `notFound()` или `redirect()` | Серверный `await` | [Серверный await](/docs/data/rest/strategies/server-await) | | Данные обязательны для первого HTML, SEO, `notFound()` или `redirect()` | Серверный `await` | [Серверный await](/docs/applied/data-fetch/server-await) |
| Несколько независимых данных нужны до рендера | Запуск промисов + `Promise.all` | [Параллельные серверные запросы](/docs/data/rest/strategies/parallel-server-requests) | | Несколько независимых данных нужны до рендера | Запуск промисов + `Promise.all` | [Параллельные серверные запросы](/docs/applied/data-fetch/parallel-server-requests) |
| Часть UI можно загрузить отдельно | Передача промиса ниже + `Suspense` | [Передача промиса ниже](/docs/data/rest/strategies/pass-promise-down) | | Часть UI можно загрузить отдельно | Передача промиса ниже + `Suspense` | [Передача промиса ниже](/docs/applied/data-fetch/pass-promise-down) |
| Client Component должен получить данные сразу из SWR | Начальные данные для клиентских хуков | [Начальные данные для клиентских хуков](/docs/data/rest/strategies/client-hooks-initial-data) | | Client Component должен получить данные сразу из SWR | Начальные данные для клиентских хуков | [Начальные данные для клиентских хуков](/docs/applied/data-fetch/client-hooks-initial-data) |
| Данные зависят от client state | Клиентский GET-хук | [Клиентский GET-хук](/docs/data/rest/strategies/client-get-hook) | | Данные зависят от client state | Клиентский GET-хук | [Клиентский GET-хук](/docs/applied/data-fetch/client-get-hook) |
| Нужно объединить несколько запросов или вычислить `isAuth`, `canEdit`, `hasPets` | Business-композиция | [Business-композиция](/docs/data/rest/strategies/business-composition) | | Нужно объединить несколько запросов или вычислить `isAuth`, `canEdit`, `hasPets` | Business-композиция | [Business-композиция](/docs/applied/data-fetch/business-composition) |
## Правило выбора ## Правило выбора
@@ -86,8 +86,8 @@ useEffect(() => {
// Плохо — useSWR в компоненте // Плохо — useSWR в компоненте
const { data } = useSWR( const { data } = useSWR(
['pet-store-api', 'pet', 'list', status], ['pet-store-api', '/pet/findByStatus?status=available'],
() => petStoreApi.pet.findPetsByStatus({ status }), () => petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Available }),
) )
// Плохо — бизнес-флаг внутри GET-хука REST-клиента // Плохо — бизнес-флаг внутри GET-хука REST-клиента

View File

@@ -17,13 +17,19 @@ keywords: [rest, promise.all, параллельные запросы, server co
## Хорошо ## Хорошо
```tsx ```tsx
import { petStoreApi } from 'infra/pet-store-api' import { petStoreApi, StatusEnum } from 'infra/pet-store-api'
import { PetsDashboardScreen } from 'screens/pets-dashboard' import { PetsDashboardScreen } from 'screens/pets-dashboard'
export default async function PetsDashboardPage() { export default async function PetsDashboardPage() {
const availablePetsPromise = petStoreApi.pet.findPetsByStatus({ status: 'available' }) const availablePetsPromise = petStoreApi.pet.findPetsByStatus({
const pendingPetsPromise = petStoreApi.pet.findPetsByStatus({ status: 'pending' }) status: StatusEnum.Available,
const soldPetsPromise = petStoreApi.pet.findPetsByStatus({ status: 'sold' }) })
const pendingPetsPromise = petStoreApi.pet.findPetsByStatus({
status: StatusEnum.Pending,
})
const soldPetsPromise = petStoreApi.pet.findPetsByStatus({
status: StatusEnum.Sold,
})
const [availablePets, pendingPets, soldPets] = await Promise.all([ const [availablePets, pendingPets, soldPets] = await Promise.all([
availablePetsPromise, availablePetsPromise,
@@ -45,9 +51,15 @@ export default async function PetsDashboardPage() {
```tsx ```tsx
export default async function PetsDashboardPage() { export default async function PetsDashboardPage() {
const availablePets = await petStoreApi.pet.findPetsByStatus({ status: 'available' }) const availablePets = await petStoreApi.pet.findPetsByStatus({
const pendingPets = await petStoreApi.pet.findPetsByStatus({ status: 'pending' }) status: StatusEnum.Available,
const soldPets = await petStoreApi.pet.findPetsByStatus({ status: 'sold' }) })
const pendingPets = await petStoreApi.pet.findPetsByStatus({
status: StatusEnum.Pending,
})
const soldPets = await petStoreApi.pet.findPetsByStatus({
status: StatusEnum.Sold,
})
return ( return (
<PetsDashboardScreen <PetsDashboardScreen
@@ -68,8 +80,8 @@ export default async function PetsDashboardPage() {
```tsx ```tsx
export default async function OrderPage({ params }: OrderPageProps) { export default async function OrderPage({ params }: OrderPageProps) {
const { id } = await params const { id } = await params
const order = await petStoreApi.store.getOrderById(Number(id)) const order = await petStoreApi.store.getOrderById({ orderId: Number(id) })
const pet = await petStoreApi.pet.getPetById(order.petId) const pet = await petStoreApi.pet.getPetById({ petId: order.petId })
return <OrderScreen order={order} pet={pet} /> return <OrderScreen order={order} pet={pet} />
} }
@@ -79,4 +91,4 @@ export default async function OrderPage({ params }: OrderPageProps) {
## Когда выбрать другую стратегию ## Когда выбрать другую стратегию
Если часть данных не обязательна для первого блока UI, можно запустить промис выше и передать его ниже: [Передача промиса ниже](/docs/data/rest/strategies/pass-promise-down). Если часть данных не обязательна для первого блока UI, можно запустить промис выше и передать его ниже: [Передача промиса ниже](/docs/applied/data-fetch/pass-promise-down).

View File

@@ -19,13 +19,15 @@ keywords: [rest, promise, suspense, streaming, server components]
```tsx ```tsx
// src/app/(routes)/pets/page.tsx // src/app/(routes)/pets/page.tsx
import { Suspense } from 'react' 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 { PetListSection } from 'widgets/pet-list-section'
import { PetListSkeleton } from 'widgets/pet-list-section' import { PetListSkeleton } from 'widgets/pet-list-section'
import type { Pet } from 'infra/pet-store-api' import type { Pet } from 'infra/pet-store-api'
export default function PetsPage() { export default function PetsPage() {
const petsPromise = petStoreApi.pet.findPetsByStatus({ status: 'available' }) const petsPromise = petStoreApi.pet.findPetsByStatus({
status: StatusEnum.Available,
})
return ( return (
<main> <main>
@@ -50,7 +52,7 @@ async function AvailablePets({ petsPromise }: { petsPromise: Promise<Pet[]> }) {
Эта стратегия остаётся серверной. Не используйте её как замену GET-хукам в Client Components. Эта стратегия остаётся серверной. Не используйте её как замену GET-хукам в Client Components.
Если данные должны попасть в клиентский SWR-хук, используйте [Начальные данные для клиентских хуков](/docs/data/rest/strategies/client-hooks-initial-data). Если данные должны попасть в клиентский SWR-хук, используйте [Начальные данные для клиентских хуков](/docs/applied/data-fetch/client-hooks-initial-data).
## Что не делать ## Что не делать

View File

@@ -29,12 +29,12 @@ SSR/dynamic rendering нужен, когда данные зависят от т
```tsx ```tsx
// src/app/(routes)/pets/page.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' import { PetsScreen } from 'screens/pets'
export default async function PetsPage() { export default async function PetsPage() {
const pets = await petStoreApi.pet.findPetsByStatus({ const pets = await petStoreApi.pet.findPetsByStatus({
status: 'available', status: StatusEnum.Available,
}) })
return <PetsScreen pets={pets} /> return <PetsScreen pets={pets} />
@@ -57,7 +57,7 @@ type PetPageProps = {
export default async function PetPage({ params }: PetPageProps) { export default async function PetPage({ params }: PetPageProps) {
const { id } = await params 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) { if (!pet) {
notFound() notFound()
@@ -73,7 +73,7 @@ export default async function PetPage({ params }: PetPageProps) {
```tsx ```tsx
// Плохо — хуки нельзя вызывать в Server Component // Плохо — хуки нельзя вызывать в Server Component
const { data } = useGetPetList('available') const { data } = useGetPetList({ status: StatusEnum.Available })
// Плохо — прямой fetch в обход клиента // Плохо — прямой fetch в обход клиента
const response = await fetch('https://petstore3.swagger.io/api/v3/pet/findByStatus') const response = await fetch('https://petstore3.swagger.io/api/v3/pet/findByStatus')
@@ -83,6 +83,6 @@ const response = await fetch('https://petstore3.swagger.io/api/v3/pet/findByStat
## Когда выбрать другую стратегию ## Когда выбрать другую стратегию
- Несколько независимых запросов — [Параллельные серверные запросы](/docs/data/rest/strategies/parallel-server-requests). - Несколько независимых запросов — [Параллельные серверные запросы](/docs/applied/data-fetch/parallel-server-requests).
- Часть UI можно грузить отдельно — [Передача промиса ниже](/docs/data/rest/strategies/pass-promise-down). - Часть UI можно грузить отдельно — [Передача промиса ниже](/docs/applied/data-fetch/pass-promise-down).
- Данные нужны клиентскому хуку сразу после гидрации — [Начальные данные для клиентских хуков](/docs/data/rest/strategies/client-hooks-initial-data). - Данные нужны клиентскому хуку сразу после гидрации — [Начальные данные для клиентских хуков](/docs/applied/data-fetch/client-hooks-initial-data).

View File

@@ -134,7 +134,7 @@ export default async function FeedLayout({ children }: FeedLayoutProps) {
} }
``` ```
Подробнее о стратегиях запросов и начальных данных для клиентских хуков: [REST → Стратегии получения данных](/docs/data/rest/strategies/), [REST → Начальные данные для клиентских хуков](/docs/data/rest/strategies/client-hooks-initial-data). Подробнее о стратегиях запросов и начальных данных для клиентских хуков: [Получение данных](/docs/applied/data-fetch/), [Начальные данные для клиентских хуков](/docs/applied/data-fetch/client-hooks-initial-data).
## Инициализация состояния ## Инициализация состояния

View File

@@ -0,0 +1,50 @@
---
title: REST-клиент
description: Настройка REST-клиента сервиса для работы с внешним API.
keywords: [rest, api, данные, infra, клиент, swr, стратегии]
---
# REST-клиент
Настройка REST-клиента сервиса для работы с внешним API.
## Настройка
Для каждого внешнего сервиса создаётся отдельный API-клиент: `pet-store-api`, `billing-api`, `maps-api`.
На этом этапе внешний API оформляется как модуль слоя `infra/`.
Клиент отвечает за:
- генерацию или ручное описание методов API;
- настройку `baseUrl`;
- заголовки и авторизацию;
- обработку ошибок;
- кастомизацию и расширение типов;
- GET-хуки для клиентских компонентов;
- прямое использование методов клиента в серверном коде и submit-функциях;
- публичный API модуля.
Если у API есть OpenAPI-спецификация — клиент генерируется автоматически. Если OpenAPI нет или он неполный — клиент создаётся вручную.
GET-хуки относятся к клиенту, потому что это прозрачные SWR-обёртки над GET-методами этого клиента.
Подробнее:
- [Настройка REST-клиента](/docs/applied/rest-client/setup/)
- [Автогенерация из OpenAPI](/docs/applied/rest-client/setup/auto)
- [Ручное создание](/docs/applied/rest-client/setup/manual)
- [GET-хуки REST-клиента](/docs/applied/rest-client/setup/hooks)
- [Использование REST-клиента](/docs/applied/rest-client/usage)
## Как читать раздел
Если API ещё не подключён — начните с [Настройки REST-клиента](/docs/applied/rest-client/setup/).
Если клиент уже создан и нужно вызвать его методы — откройте [Использование REST-клиента](/docs/applied/rest-client/usage).
Если клиент уже есть, но непонятно как получить данные — начните с раздела [Получение данных](/docs/applied/data-fetch/).
Если данные нужны в Client Component — сначала проверьте, есть ли [GET-хук REST-клиента](/docs/applied/rest-client/setup/hooks).
Если в коде появляется бизнес-смысл вроде `isAuth`, `canEdit`, `hasAccess` — это уже не REST-клиент, а `business/`.

View File

@@ -1,10 +1,14 @@
--- ---
title: Автогенерация из OpenAPI title: Автогенерация REST-клиента
description: Генерация REST-клиента из OpenAPI-спецификации через @gromlab/api-codegen. description: Генерация REST-клиента из OpenAPI-спецификации.
keywords: [rest, openapi, api-codegen, автогенерация, generated, npx] keywords: [rest, openapi, api-codegen, автогенерация, generated, npx]
--- ---
# Автогенерация из OpenAPI # Автогенерация REST-клиента
Генерация REST-клиента из OpenAPI-спецификации.
## Когда использовать
Автогенерация используется, когда у API есть актуальная OpenAPI-спецификация. Генератор создаёт TypeScript-клиент, типы и методы API, а разработчик вручную добавляет настройку клиента и GET-хуки. Автогенерация используется, когда у API есть актуальная OpenAPI-спецификация. Генератор создаёт TypeScript-клиент, типы и методы API, а разработчик вручную добавляет настройку клиента и GET-хуки.
@@ -66,12 +70,39 @@ src/infra/pet-store-api/generated/
Для Petstore нужны GET-операции вида: Для Petstore нужны GET-операции вида:
```ts ```ts
petStoreApi.pet.findPetsByStatus(...) petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Available })
petStoreApi.pet.getPetById(...) petStoreApi.pet.getPetById({ petId: 10 })
``` ```
Точные сигнатуры зависят от OpenAPI-схемы и версии генератора. В рабочих задачах всегда сверяйтесь с generated-файлом. Точные сигнатуры зависят от OpenAPI-схемы и версии генератора. В рабочих задачах всегда сверяйтесь с generated-файлом.
## Алгоритм для агента
После генерации агент должен действовать по шагам:
1. Открыть `generated/{service-name}.generated.ts`.
2. Найти фактические имена GET-методов клиента.
3. Для каждого нужного GET-метода найти generated-тип параметров и тип ответа.
4. Создать или обновить `client.ts` только для настройки транспорта и экспорта инстанса клиента.
5. Создать GET-хуки только для реально нужных GET-методов, не для всех методов API на всякий случай.
6. Для каждого GET-хука создать key-функцию формата `[serviceName, endpoint]`.
7. В key-функции вернуть `null`, если обязательные параметры не готовы.
8. В хуке принять `params?: GeneratedParams | null` и `config?: SWRConfiguration<Data>`.
9. В fetcher вызвать generated-метод клиента с `params as GeneratedParams`.
10. Экспортировать хук и key-функцию из `hooks/index.ts`.
11. Экспортировать наружу только нужные generated-типы, generated enum, DTO и `hooks` через корневой `index.ts`.
Что агент не должен делать:
- Не использовать ключ `--swr` генератора.
- Не править `generated/*.generated.ts` руками.
- Не добавлять GET-хуки для POST, PUT, PATCH, DELETE.
- Не добавлять бизнес-флаги, тосты, редиректы и UI-состояние в GET-хук.
- Не создавать словари enum-маппинга внутри GET-хука.
- Не объявлять DTO и response-типы в файле хука.
- Не вызывать `useSWR` условно.
- Не добавлять `throw` в fetcher для неготовых params.
## `client.ts` ## `client.ts`
Сгенерированный код не должен напрямую использоваться из приложения. Сначала создаётся настроенный инстанс клиента. Сгенерированный код не должен напрямую использоваться из приложения. Сначала создаётся настроенный инстанс клиента.
@@ -80,8 +111,14 @@ petStoreApi.pet.getPetById(...)
// src/infra/pet-store-api/client.ts // src/infra/pet-store-api/client.ts
import { Api, HttpClient } from './generated/pet-store-api.generated' 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({ const httpClient = new HttpClient({
baseUrl: 'https://petstore3.swagger.io/api/v3', baseUrl,
baseApiParams: { baseApiParams: {
secure: false, secure: false,
headers: { headers: {
@@ -93,10 +130,47 @@ const httpClient = new HttpClient({
export const petStoreApi = new Api(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`-типы. Он только настраивает транспорт и экспортирует инстанс клиента. `client.ts` не содержит расширения типов, `declare module` и `Extended`-типы. Он только настраивает транспорт и экспортирует инстанс клиента.
## GET-хуки
GET-хуки пишутся вручную после проверки generated-методов.
Пример для generated-метода `petStoreApi.pet.getPetById({ petId })`:
```ts
// src/infra/pet-store-api/hooks/use-get-pet-detail.hook.ts
import type { SWRConfiguration } from 'swr'
import useSWR from 'swr'
import { petStoreApi } from '../client'
import type { GetPetByIdParams, Pet } from '../generated/pet-store-api.generated'
export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
if (!params?.petId) {
return null
}
return ['pet-store-api', `/pet/${params.petId}`] as const
}
/**
* Получает детальную карточку питомца с кешированием результата.
*/
export const useGetPetDetail = (
params?: GetPetByIdParams | null,
config?: SWRConfiguration<Pet>,
) => {
const key = getPetDetailKey(params)
const fetcher = () => petStoreApi.pet.getPetById(params as GetPetByIdParams)
return useSWR<Pet>(key, fetcher, config)
}
```
Подробный контракт key-функций, `params`, `config` и запретов описан в разделе [GET-хуки REST-клиента](/docs/applied/rest-client/setup/hooks).
## Расширение сгенерированных типов ## Расширение сгенерированных типов
Сгенерированный файл не правится руками. Если OpenAPI-спецификация неполная или генератор дал слишком общий тип (`object`, `unknown`, отсутствующее поле), расширения живут в `types/`. Сгенерированный файл не правится руками. Если OpenAPI-спецификация неполная или генератор дал слишком общий тип (`object`, `unknown`, отсутствующее поле), расширения живут в `types/`.
@@ -160,7 +234,12 @@ export type { TermRecordItemExtended } from './term'
```ts ```ts
// src/infra/pet-store-api/index.ts // src/infra/pet-store-api/index.ts
export { petStoreApi } from './client' 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' export * from './hooks'
``` ```
@@ -190,4 +269,4 @@ npm run codegen:pet-store-api
## Следующий шаг ## Следующий шаг
После генерации и настройки `client.ts` проверьте серверный вызов метода клиента или добавьте [GET-хук REST-клиента](/docs/data/rest/clients/hooks) для Client Components. После генерации и настройки `client.ts` проверьте [использование REST-клиента](/docs/applied/rest-client/usage) или добавьте [GET-хук REST-клиента](/docs/applied/rest-client/setup/hooks) для Client Components.

View File

@@ -0,0 +1,313 @@
---
title: GET-хуки REST-клиента
description: Прозрачные SWR-обёртки над GET-методами REST-клиента.
keywords: [rest, swr, get-хуки, client components, infra]
---
# GET-хуки REST-клиента
Прозрачные SWR-обёртки над GET-методами REST-клиента.
## Зачем нужны
GET-хуки нужны, чтобы Client Components получали REST-данные через SWR, но не работали с `useSWR`, ключами кеша и fetcher напрямую.
## Где лежат
GET-хуки принадлежат REST-клиенту конкретного сервиса и живут рядом с ним:
```text
src/infra/
└── pet-store-api/
├── client.ts
├── generated/
├── hooks/
│ ├── use-get-pet-list.hook.ts
│ ├── use-get-pet-detail.hook.ts
│ └── index.ts
├── types/
└── index.ts
```
## Контракт
- Один GET-хук = один GET-метод клиента.
- Имя GET-хука начинается с `useGet`: `useGetPetList`, `useGetPetDetail`.
- Имя файла начинается с `use-get`: `use-get-pet-list.hook.ts`.
- Хук принимает `params?: GeneratedParams | null` и `config?: SWRConfiguration<Data>`.
- Для GET-метода без параметров хук принимает только `config?: SWRConfiguration<Data>`.
- Key-функция принимает те же `params`, что и хук.
- Key-функция возвращает `null`, если обязательные параметры не готовы.
- Проверка готовности запроса живёт в key-функции, а не в теле хука.
- Хук вызывает `useSWR` один раз и безусловно.
- Fetcher не проверяет `null`, не бросает ошибку и не вызывает метод клиента с `null`.
- Внутри только SWR-механика: key, fetcher, `useSWR`, `config`.
- Хук возвращает тип ответа API: generated-тип или DTO из `types/`.
- Хук не объединяет несколько запросов.
- Хук не маппит DTO в доменную модель.
- Хук не вычисляет бизнес-флаги: `isAuth`, `canEdit`, `hasAccess`, `hasPets`.
- Хук не вызывает тосты, модалки, редиректы и не пишет UI-состояние.
## Формат SWR-ключа
SWR-ключ GET-хука всегда создаётся отдельной экспортируемой функцией.
Формат ключа:
```ts
['pet-store-api', '/pet/10'] as const
```
- Первый элемент — имя API-сервиса или REST-клиента в `kebab-case`.
- Второй элемент — endpoint запроса: path и query string.
- Key-функция возвращает `null`, когда запрос нельзя выполнять.
- Key-функция нужна и GET-хуку, и `SWRConfig fallback`.
- Не используйте произвольные части вроде `['pet-store-api', 'pet', 'detail', params]`.
- Не используйте только строку endpoint без имени сервиса.
Примеры ключей:
```ts
export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
if (!params?.petId) {
return null
}
return ['pet-store-api', `/pet/${params.petId}`] as const
}
```
```ts
export const getPetListKey = (params?: FindPetsByStatusParams | null) => {
if (!params?.status) {
return null
}
return ['pet-store-api', `/pet/findByStatus?status=${params.status}`] as const
}
```
```ts
export const getPetListByTagsKey = (params?: FindPetsByTagsParams | null) => {
if (!params?.tags.length) {
return null
}
return ['pet-store-api', `/pet/findByTags?tags=${params.tags.join(',')}`] as const
}
```
Если API допускает `0` как валидный идентификатор, не используйте проверку `!params?.id`. В таком случае проверяйте `null` и `undefined` явно.
## Пример списка
```ts
// src/infra/pet-store-api/hooks/use-get-pet-list.hook.ts
import type { SWRConfiguration } from 'swr'
import useSWR from 'swr'
import { petStoreApi } from '../client'
import type {
FindPetsByStatusParams,
Pet,
} from '../generated/pet-store-api.generated'
export const getPetListKey = (params?: FindPetsByStatusParams | null) => {
if (!params?.status) {
return null
}
return ['pet-store-api', `/pet/findByStatus?status=${params.status}`] as const
}
/**
* Получает список питомцев по статусу.
*/
export const useGetPetList = (
params?: FindPetsByStatusParams | null,
config?: SWRConfiguration<Pet[]>,
) => {
const key = getPetListKey(params)
const fetcher = () => petStoreApi.pet.findPetsByStatus(
params as FindPetsByStatusParams,
)
return useSWR<Pet[]>(key, fetcher, config)
}
```
`params as FindPetsByStatusParams` допустим только в fetcher: готовность параметров проверена в key-функции, а при `key = null` SWR не вызывает fetcher.
## Пример detail-запроса
```ts
// src/infra/pet-store-api/hooks/use-get-pet-detail.hook.ts
import type { SWRConfiguration } from 'swr'
import useSWR from 'swr'
import { petStoreApi } from '../client'
import type { GetPetByIdParams, Pet } from '../generated/pet-store-api.generated'
export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
if (!params?.petId) {
return null
}
return ['pet-store-api', `/pet/${params.petId}`] as const
}
/**
* Получает детальную карточку питомца с кешированием результата.
*/
export const useGetPetDetail = (
params?: GetPetByIdParams | null,
config?: SWRConfiguration<Pet>,
) => {
const key = getPetDetailKey(params)
const fetcher = () => petStoreApi.pet.getPetById(params as GetPetByIdParams)
return useSWR<Pet>(key, fetcher, config)
}
```
## Пример без параметров
```ts
// src/infra/pet-store-api/hooks/use-get-store-inventory.hook.ts
import type { SWRConfiguration } from 'swr'
import useSWR from 'swr'
import { petStoreApi } from '../client'
import type { StoreInventory } from '../types'
export const getStoreInventoryKey = () => {
return ['pet-store-api', '/store/inventory'] as const
}
/**
* Получает инвентарь магазина.
*/
export const useGetStoreInventory = (
config?: SWRConfiguration<StoreInventory>,
) => {
return useSWR<StoreInventory>(
getStoreInventoryKey(),
() => petStoreApi.store.getInventory(),
config,
)
}
```
Если generated-метод возвращает безымянный тип вроде `Record<string, number>`, а тип нужен наружу, вынесите его в `types/`.
## Отложенный запрос
GET-хук может принимать `null` или `undefined` для обязательных параметров. Это означает, что параметры ещё не готовы и запрос выполнять нельзя.
```ts
const key = getPetDetailKey(params)
```
Если `params` не готов, key-функция вернёт `null`. SWR не вызовет fetcher для `null`-ключа.
Не добавляйте отдельные `isReady`, `throw new Error(...)` и условный вызов `useSWR`.
## Экспорт
```ts
// src/infra/pet-store-api/hooks/index.ts
export { getPetListKey, useGetPetList } from './use-get-pet-list.hook'
export { getPetDetailKey, useGetPetDetail } from './use-get-pet-detail.hook'
export {
getStoreInventoryKey,
useGetStoreInventory,
} from './use-get-store-inventory.hook'
```
```ts
// src/infra/pet-store-api/index.ts
export { petStoreApi } from './client'
export type {
FindPetsByStatusParams,
GetPetByIdParams,
Pet,
} from './generated/pet-store-api.generated'
export { PetStatusEnum, StatusEnum } from './generated/pet-store-api.generated'
export * from './hooks'
export type { StoreInventory } from './types'
```
Наружу импортируют только из `infra/pet-store-api`, не из `generated/` и не из `hooks/` напрямую.
## Где заканчивается infra
```ts
// Хорошо: infra, прозрачный GET-хук
const { data: pets } = useGetPetList({ status: StatusEnum.Available })
```
```ts
// Хорошо: business, доменная интерпретация
export const useAvailablePets = () => {
const query = useGetPetList({ status: StatusEnum.Available })
return {
...query,
hasPets: Boolean(query.data?.length),
}
}
```
`hasPets` — не часть GET-запроса, поэтому он не добавляется в `useGetPetList`.
## Что запрещено
```ts
// Плохо — useSWR в компоненте
const { data } = useSWR(
['pet-store-api', '/pet/findByStatus?status=available'],
() => petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Available }),
)
// Плохо — проверка готовности размазана по хуку
export const useGetPetDetail = (params?: GetPetByIdParams | null) => {
const key = params?.petId ? getPetDetailKey(params) : null
const fetcher = () => {
if (!params?.petId) {
throw new Error('Pet id is required')
}
return petStoreApi.pet.getPetById(params)
}
return useSWR<Pet>(key, fetcher)
}
// Плохо — условный вызов useSWR нарушает rules of hooks
export const useGetPetDetail = (params?: GetPetByIdParams | null) => {
const key = getPetDetailKey(params)
if (key === null) {
return useSWR(null, null)
}
return useSWR(key, () => petStoreApi.pet.getPetById(params))
}
// Плохо — несколько GET внутри infra-хука
export const usePetDashboard = () => {
const available = useGetPetList({ status: StatusEnum.Available })
const sold = useGetPetList({ status: StatusEnum.Sold })
return { available, sold }
}
// Плохо — бизнес-флаг внутри GET-хука REST-клиента
export const useGetPetList = (params?: FindPetsByStatusParams | null) => {
const query = useSWR(...)
return {
...query,
hasPets: Boolean(query.data?.length),
}
}
```
Подробное потребление таких хуков описано в стратегии [Клиентский GET-хук](/docs/applied/data-fetch/client-get-hook).

View File

@@ -1,14 +1,18 @@
--- ---
title: Создание клиента title: Настройка REST-клиента
description: Из чего состоит REST-клиент и какие части нужно подготовить перед использованием API. description: Подготовка REST-клиента сервиса к использованию.
keywords: [rest, клиент, infra, методы, openapi, get-хуки, swr] keywords: [rest, клиент, infra, методы, openapi, get-хуки, swr]
--- ---
# Создание клиента # Настройка REST-клиента
Подготовка REST-клиента сервиса к использованию.
## Что настраиваем
REST-клиент — это infra-модуль, через который проект работает с внешним REST API. REST-клиент — это infra-модуль, через который проект работает с внешним REST API.
На этом этапе нужно подготовить клиент сервиса: создать оболочку клиента, получить методы API и добавить GET-хуки для клиентских компонентов. На этапе настройки нужно подготовить клиент сервиса: оболочку клиента, методы API и GET-хуки для клиентских компонентов.
## Из чего состоит клиент ## Из чего состоит клиент
@@ -28,6 +32,8 @@ REST-клиент состоит из трёх основных частей:
`client.ts` — только сборочная точка клиента. В нём не размещаются DTO, `declare module`, `Extended`-типы, GET-хуки и бизнес-логика. `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. Методы описывают конкретные запросы к API.
@@ -39,8 +45,8 @@ REST-клиент состоит из трёх основных частей:
Подробности: Подробности:
- [Автогенерация из OpenAPI](/docs/data/rest/clients/auto) - [Автогенерация из OpenAPI](/docs/applied/rest-client/setup/auto)
- [Ручное создание](/docs/data/rest/clients/manual) - [Ручное создание](/docs/applied/rest-client/setup/manual)
## GET-хуки ## GET-хуки
@@ -50,9 +56,13 @@ REST-клиент состоит из трёх основных частей:
GET-хуки именуются с префиксом `useGet`: `useGetPetList`, `useGetPetDetail`, `useGetCurrentUser`. GET-хуки именуются с префиксом `useGet`: `useGetPetList`, `useGetPetDetail`, `useGetCurrentUser`.
Каждый GET-хук имеет экспортируемую key-функцию. SWR-ключ всегда имеет формат `[serviceName, endpoint]`: например `['pet-store-api', '/pet/10']`.
Хук принимает generated-параметры метода и SWR-настройки: `params?: GetPetByIdParams | null`, `config?: SWRConfiguration<Pet>`.
Подробности: Подробности:
- [GET-хуки REST-клиента](/docs/data/rest/clients/hooks) - [GET-хуки REST-клиента](/docs/applied/rest-client/setup/hooks)
## Структура модуля ## Структура модуля
@@ -61,15 +71,18 @@ src/infra/{service-name}/
├── client.ts # самописная оболочка и инстанс клиента ├── client.ts # самописная оболочка и инстанс клиента
├── generated/ или methods/ # методы API ├── generated/ или methods/ # методы API
├── hooks/ # GET-хуки REST-клиента ├── hooks/ # GET-хуки REST-клиента
├── types/ # DTO, типы API и расширения типов ├── types/ # DTO, именованные response-типы и расширения типов
├── errors/ # ошибки API, если нужны ├── errors/ # ошибки API, если нужны
└── index.ts # публичный API └── index.ts # публичный API
``` ```
`index.ts` — единственная точка входа в REST-модуль для внешнего кода. `index.ts` — единственная точка входа в REST-модуль для внешнего кода.
Если generated-метод возвращает безымянный тип вроде `Record<string, number>`, а этот тип нужен снаружи, вынесите его в `types/`. Не объявляйте DTO внутри `hooks/use-get-*.hook.ts`.
## Что делаем дальше ## Что делаем дальше
1. Создайте методы клиента: [Автогенерация из OpenAPI](/docs/data/rest/clients/auto) или [Ручное создание](/docs/data/rest/clients/manual). 1. Создайте методы клиента: [Автогенерация из OpenAPI](/docs/applied/rest-client/setup/auto) или [Ручное создание](/docs/applied/rest-client/setup/manual).
2. Добавьте GET-хуки для GET-запросов: [GET-хуки REST-клиента](/docs/data/rest/clients/hooks). 2. Добавьте GET-хуки для GET-запросов: [GET-хуки REST-клиента](/docs/applied/rest-client/setup/hooks).
3. После создания клиента переходите к [Стратегиям получения данных](/docs/data/rest/strategies/). 3. Проверьте прямые вызовы клиента: [Использование REST-клиента](/docs/applied/rest-client/usage).
4. После настройки клиента переходите к [Получению данных](/docs/applied/data-fetch/).

View File

@@ -1,10 +1,14 @@
--- ---
title: Ручное создание title: Ручное создание REST-клиента
description: Создание REST-клиента вручную, когда OpenAPI нет или он неполный. description: Создание REST-клиента вручную, когда OpenAPI нет или он неполный.
keywords: [rest, ручной клиент, fetch, methods, dto, errors, infra] keywords: [rest, ручной клиент, fetch, methods, dto, errors, infra]
--- ---
# Ручное создание # Ручное создание REST-клиента
Создание REST-клиента вручную, когда OpenAPI нет или он неполный.
## Когда использовать
Ручной REST-клиент используется, когда у API нет OpenAPI-спецификации или она недостаточно точная для автогенерации. Ручной REST-клиент используется, когда у API нет OpenAPI-спецификации или она недостаточно точная для автогенерации.
@@ -159,8 +163,14 @@ export function postsMethods(client: PetProjectApiClient) {
import { PetProjectApiClient } from './client' import { PetProjectApiClient } from './client'
import { postsMethods } from './methods/posts' 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( const client = new PetProjectApiClient(
process.env.NEXT_PUBLIC_API_URL ?? '', baseUrl,
{ 'Content-Type': 'application/json' }, { 'Content-Type': 'application/json' },
) )
@@ -180,8 +190,9 @@ export * from './hooks'
- `fetch` используется только внутри базового клиента. - `fetch` используется только внутри базового клиента.
- DTO запросов и ответов живут в `types/`. - DTO запросов и ответов живут в `types/`.
- `client.ts` не содержит DTO, GET-хуки и бизнес-логику. - `client.ts` не содержит DTO, GET-хуки и бизнес-логику.
- `baseUrl` берётся из обязательной env-переменной без fallback-значения в коде.
- Методы лежат в `methods/` и возвращают DTO. - Методы лежат в `methods/` и возвращают DTO.
- GET-хуки добавляются отдельно в `hooks/`, если данные нужны в Client Components. - GET-хуки добавляются отдельно в `hooks/`, если данные нужны в Client Components.
- Доменные типы и маппинг DTO живут не в REST-клиенте, а в `business/`. - Доменные типы и маппинг DTO живут не в REST-клиенте, а в `business/`.
Следующий шаг: [GET-хуки REST-клиента](/docs/data/rest/clients/hooks) или [Стратегии получения данных](/docs/data/rest/strategies/). Следующий шаг: [Использование REST-клиента](/docs/applied/rest-client/usage), [GET-хуки REST-клиента](/docs/applied/rest-client/setup/hooks) или [Получение данных](/docs/applied/data-fetch/).

View File

@@ -0,0 +1,21 @@
---
title: Использование REST-клиента
description: Как вызвать готовый REST-клиент в функции.
keywords: [rest, api client, submit, generated, pet-store-api]
---
# Использование REST-клиента
Как вызвать готовый REST-клиент в функции.
## Пример
```ts
import { petStoreApi } from 'infra/pet-store-api'
export const getPet = async (petId: number) => {
const pet = await petStoreApi.pet.getPetById({ petId })
console.log(pet)
}
```

View File

@@ -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](/docs/data/rest/) — обзор раздела: создание клиента и использование.
- **Создание клиента** — как оформляется REST API в проекте:
- [Обзор](/docs/data/rest/clients/) — когда нужен клиент и как выбрать подход.
- [Автогенерация из OpenAPI](/docs/data/rest/clients/auto) — для API с OpenAPI-спецификацией, через `@gromlab/api-codegen`.
- [Ручное создание](/docs/data/rest/clients/manual) — для API без схемы, клиент пишется и поддерживается руками.
- [GET-хуки REST-клиента](/docs/data/rest/clients/hooks) — прозрачные SWR-обёртки над GET-методами клиента.
- **Использование** — как получать данные через готовый клиент:
- [Стратегии получения данных](/docs/data/rest/strategies/) — как выбрать способ получения данных под ситуацию.
- [Серверный await](/docs/data/rest/strategies/server-await) — прямой `await` метода клиента в Server Components.
- [Параллельные серверные запросы](/docs/data/rest/strategies/parallel-server-requests) — запуск независимых серверных запросов без waterfall.
- [Передача промиса ниже](/docs/data/rest/strategies/pass-promise-down) — серверный стриминг через промис и `Suspense`.
- [Начальные данные для клиентских хуков](/docs/data/rest/strategies/client-hooks-initial-data) — серверный промис в `SWRConfig fallback`.
- [Клиентский GET-хук](/docs/data/rest/strategies/client-get-hook) — получение данных в Client Components через готовый GET-хук.
- [Business-композиция](/docs/data/rest/strategies/business-composition) — доменная интерпретация и композиция REST-данных.
### Realtime
Канал push-данных: WebSocket, SSE, событийные шины. Транспорт не зашит в правила — важна абстракция «подписка».
- [Realtime](/docs/data/realtime) — клиент realtime в `infra/`, потребление через `useSWRSubscription` или прямые подписки.
## Что даёт раздел
После прочтения раздела понятно:
- Где живёт код работы с API и почему именно там.
- Когда генерировать клиент автоматически, а когда писать вручную, и как структурирован каждый из вариантов.
- Какие GET-хуки относятся к REST-клиенту и почему они живут в `infra/{service-name}/hooks/`.
- Как выбрать стратегию получения REST-данных под конкретную ситуацию.
- Как подключать realtime-источники в общую модель работы с данными.
- Какие правила обязательны и какие отклонения допустимы.
## Что не входит в раздел
- **Глобальное состояние UI** — Stores, формы, фичефлаги. Это [Stores](/docs/applied/stores).
- **Доменная логика** — как данные превращаются в сценарии бизнеса. Это слой `business/` в [Архитектуре](/docs/basics/architecture/).
- **Хуки общего назначения** — переиспользуемые хуки UI, не привязанные к конкретному API. Отдельный прикладной раздел для них пока не ведётся.

View File

@@ -1,79 +0,0 @@
---
title: Realtime
description: "Работа с push-данными от сервера: подписки и события."
keywords: [realtime, websocket, sse, подписка, swr subscription, useSWRSubscription, push, события]
---
# Realtime
Работа с push-данными от сервера: подписки и события.
## Принципы
- **Клиент realtime — в `infra/`** отдельным модулем по имени канала. То же правило, что и для REST: никаких прямых соединений в коде приложения.
- **Подписка — единица потребления.** Клиент даёт функцию `subscribe(topic, handler) → unsubscribe`. Внутри — конкретный транспорт.
- **Использование на клиенте — два сценария:**
- **`useSWRSubscription`** — для данных, которые показываются в UI и должны кешироваться/синхронизироваться с REST.
- **Прямая подписка** — для побочных эффектов (тосты, нотификации, аналитика), не привязанных к рендеру.
## Размещение клиента
```text
src/infra/
└── {channel-name}/
├── connection.ts # установление соединения, реконнект
├── subscribe.ts # subscribe(topic, handler) → unsubscribe
├── types.ts
└── index.ts
```
## Использование через SWR
```tsx
'use client'
import useSWRSubscription from 'swr/subscription'
import { subscribe } from 'infra/notifications'
export function NotificationCounter() {
const { data: count } = useSWRSubscription(
['notifications', 'count'],
(key, { next }) =>
subscribe('notifications.count', (value: number) => next(null, value)),
)
return <span>{count ?? 0}</span>
}
```
Плюсы: кеш и дедупликация подписки между несколькими местами рендера; единая модель данных с REST.
## Прямая подписка
Для побочных эффектов, которые не влияют на состояние UI напрямую:
```tsx
'use client'
import { useEffect } from 'react'
import { subscribe } from 'infra/notifications'
import { showToast } from 'ui/toast'
export function NotificationsToaster() {
useEffect(() => {
return subscribe('notifications.new', (notification) => {
showToast(notification.message)
})
}, [])
return null
}
```
Возврат `unsubscribe` из `useEffect` обязателен — иначе утечка подписки.
## Запрет прямых соединений
Создавать `new WebSocket(...)`, `new EventSource(...)` или подписываться на событийные шины напрямую в коде приложения — запрещено. Все соединения проходят через клиент в `infra/`.
Исключения — точечные и обоснованные (например, диагностический скрипт), помечаются комментарием.

View File

@@ -1,206 +0,0 @@
---
title: GET-хуки REST-клиента
description: Прозрачные SWR-обёртки над GET-методами REST-клиента.
keywords: [rest, swr, get-хуки, client components, infra]
---
# GET-хуки REST-клиента
GET-хуки REST-клиента — прозрачные SWR-обёртки над GET-методами API-клиента. Они нужны, чтобы Client Components получали данные с кешированием, дедупликацией и ревалидацией, не работая с `useSWR` напрямую.
## Где лежат
GET-хуки принадлежат REST-клиенту конкретного сервиса и живут рядом с ним:
```text
src/infra/
└── pet-store-api/
├── client.ts
├── generated/
├── hooks/
│ ├── use-get-pet-list.hook.ts
│ ├── use-get-pet-detail.hook.ts
│ └── index.ts
└── index.ts
```
## Контракт
- Один GET-хук = один GET-метод клиента.
- Имя GET-хука начинается с `useGet`: `useGetPetList`, `useGetPetDetail`.
- Имя файла начинается с `use-get`: `use-get-pet-list.hook.ts`.
- Хук принимает только параметры GET-метода и `config?: SWRConfiguration`.
- Что передали хуку, то он передаёт в GET-метод.
- Внутри только SWR-механика: key, fetcher, `useSWR`, `config`.
- Хук возвращает тип ответа API: generated-тип или DTO из `types/`.
- Хук не объединяет несколько запросов.
- Хук не маппит DTO в доменную модель.
- Хук не вычисляет бизнес-флаги: `isAuth`, `canEdit`, `hasAccess`, `hasPets`.
- Хук не вызывает тосты, модалки, редиректы и не пишет UI-состояние.
## Пример списка
```ts
// src/infra/pet-store-api/hooks/use-get-pet-list.hook.ts
import useSWR from 'swr'
import type { SWRConfiguration } from 'swr'
import { petStoreApi } from '../client'
import type { Pet } from '../generated/pet-store-api.generated'
export type PetStatus = 'available' | 'pending' | 'sold'
export const getPetListKey = (status: PetStatus) =>
['pet-store-api', 'pet', 'list', status] as const
/**
* Получение списка питомцев по статусу.
*/
export const useGetPetList = (status: PetStatus | null, config?: SWRConfiguration) => {
const isReady = status !== null
const key = isReady ? getPetListKey(status) : null
const fetcher = () => petStoreApi.pet.findPetsByStatus({ status })
return useSWR<Pet[]>(key, fetcher, config)
}
```
Функция `getPetListKey` нужна, чтобы один и тот же SWR-ключ использовался внутри GET-хука и при передаче начальных данных через `SWRConfig fallback`.
Пример начальных данных для клиентского хука:
```tsx
import type { ReactNode } from 'react'
import { SWRConfig, unstable_serialize } from 'swr'
import {
getPetListKey,
petStoreApi,
} from 'infra/pet-store-api'
export default function PetsLayout({ children }: { children: ReactNode }) {
const petsPromise = petStoreApi.pet.findPetsByStatus({ status: 'available' })
return (
<SWRConfig
value={{
fallback: {
[unstable_serialize(getPetListKey('available'))]: petsPromise,
},
}}
>
{children}
</SWRConfig>
)
}
```
Клиентский компонент при этом ничего не знает про preload/fallback и продолжает вызывать обычный хук:
```tsx
const { data: pets } = useGetPetList('available')
```
## Пример detail-запроса
```ts
// src/infra/pet-store-api/hooks/use-get-pet-detail.hook.ts
import useSWR from 'swr'
import type { SWRConfiguration } from 'swr'
import { petStoreApi } from '../client'
import type { Pet } from '../generated/pet-store-api.generated'
export const getPetDetailKey = (id: number) =>
['pet-store-api', 'pet', 'detail', id] as const
/**
* Получение питомца по идентификатору.
*/
export const useGetPetDetail = (id: number | null, config?: SWRConfiguration) => {
const isReady = id !== null
const key = isReady ? getPetDetailKey(id) : null
const fetcher = () => petStoreApi.pet.getPetById(id)
return useSWR<Pet>(key, fetcher, config)
}
```
## Отложенный запрос через `null`
GET-хук может принимать `null` для обязательного параметра. `null` означает, что параметр ещё не готов и запрос выполнять нельзя.
Внутри хука это выражается через `isReady`: если параметр не готов, ключ SWR становится `null`, и SWR не вызывает fetcher.
```ts
const isReady = id !== null
const key = isReady ? getPetDetailKey(id) : null
```
`null` не передаётся в метод клиента. Key-функция принимает только готовые параметры, поэтому её можно безопасно использовать для начальных данных через `SWRConfig fallback`.
Для числовых идентификаторов не используйте проверку `if (id)`: значение `0` тоже валидное число. Проверяйте явно: `id !== null`.
## Экспорт
```ts
// src/infra/pet-store-api/hooks/index.ts
export { getPetListKey, useGetPetList } from './use-get-pet-list.hook'
export type { PetStatus } from './use-get-pet-list.hook'
export { getPetDetailKey, useGetPetDetail } from './use-get-pet-detail.hook'
```
```ts
// src/infra/pet-store-api/index.ts
export { petStoreApi } from './client'
export type { Pet } from './generated/pet-store-api.generated'
export * from './hooks'
```
## Где заканчивается infra
```ts
// Хорошо: infra, прозрачный GET-хук
const { data: pets } = useGetPetList('available')
```
```ts
// Хорошо: business, доменная интерпретация
export const useAvailablePets = () => {
const query = useGetPetList('available')
return {
...query,
hasPets: Boolean(query.data?.length),
}
}
```
`hasPets` — не часть GET-запроса, поэтому он не добавляется в `useGetPetList`.
## Что запрещено
```ts
// Плохо — useSWR в компоненте
const { data } = useSWR(
['pet-store-api', 'pet', 'list', status],
() => petStoreApi.pet.findPetsByStatus({ status }),
)
// Плохо — несколько GET внутри infra-хука
export const usePetDashboard = () => {
const available = useGetPetList('available')
const sold = useGetPetList('sold')
return { available, sold }
}
// Плохо — бизнес-флаг внутри GET-хука REST-клиента
export const useGetPetList = (status: PetStatus) => {
const query = useSWR(...)
return {
...query,
hasPets: Boolean(query.data?.length),
}
}
```
Подробное потребление таких хуков описано в стратегии [Клиентский GET-хук](/docs/data/rest/strategies/client-get-hook).

View File

@@ -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-методами этого клиента.
Подробнее:
- [Создание клиента](/docs/data/rest/clients/)
- [Автогенерация из OpenAPI](/docs/data/rest/clients/auto)
- [Ручное создание](/docs/data/rest/clients/manual)
- [GET-хуки REST-клиента](/docs/data/rest/clients/hooks)
## 2. Использование
После создания клиента нужно определить рендер страницы и выбрать, как получать данные в конкретном месте приложения.
Раздел использования отвечает на вопросы:
- как понять, можно ли сохранить static/ISR;
- когда страница становится dynamic/SSR;
- когда получать данные через серверный `await`;
- когда запускать несколько серверных запросов параллельно;
- когда передавать промис ниже по дереву;
- когда передавать начальные данные клиентским GET-хукам;
- когда использовать GET-хук в клиентском компоненте;
- когда выносить композицию и бизнес-смысл в `business/`.
Подробнее:
- [Стратегии получения данных](/docs/data/rest/strategies/)
- [Серверный await](/docs/data/rest/strategies/server-await)
- [Параллельные серверные запросы](/docs/data/rest/strategies/parallel-server-requests)
- [Передача промиса ниже](/docs/data/rest/strategies/pass-promise-down)
- [Начальные данные для клиентских хуков](/docs/data/rest/strategies/client-hooks-initial-data)
- [Клиентский GET-хук](/docs/data/rest/strategies/client-get-hook)
- [Business-композиция](/docs/data/rest/strategies/business-composition)
## Как читать раздел
Если API ещё не подключён — начните с [Создания клиента](/docs/data/rest/clients/).
Если клиент уже есть, но непонятно как получить данные — начните со [Стратегий получения данных](/docs/data/rest/strategies/).
Если данные нужны в Client Component — сначала проверьте, есть ли [GET-хук REST-клиента](/docs/data/rest/clients/hooks).
Если в коде появляется бизнес-смысл вроде `isAuth`, `canEdit`, `hasAccess` — это уже не REST-клиент, а `business/`.

View File

@@ -40,22 +40,15 @@ description: Стандарты разработки фронтенд-прило
| Документирование | Как писать JSDoc: что документировать, а что нет? | | Документирование | Как писать JSDoc: что документировать, а что нет? |
| Типизация | Как типизировать: type vs interface, any/unknown? | | Типизация | Как типизировать: type vs interface, any/unknown? |
### Создание проекта
**Как начать новый проект** — варианты установки и эталонный набор инструментов.
| Раздел | Отвечает на вопрос |
|--------|-------------------|
| Создание проекта из шаблона | Как начать проект из готового шаблона? |
| Создание проекта вручную | Как поднять проект с нуля без шаблона? |
| Чистая установка Next.js | Как поставить голый Next.js под дальнейшую сборку? |
### Настройка ### Настройка
**Как сконфигурировать проект** — пошаговая настройка инструментов и инфраструктуры. **Как сконфигурировать проект** — пошаговая настройка инструментов и инфраструктуры.
| Раздел | Отвечает на вопрос | | Раздел | Отвечает на вопрос |
|--------|-------------------| |--------|-------------------|
| Создание проекта из шаблона | Как начать проект из готового шаблона? |
| Создание проекта вручную | Как поднять проект с нуля без шаблона? |
| Чистая установка Next.js | Как поставить голый Next.js под дальнейшую сборку? |
| Алиасы импортов | Как настроить алиасы импортов? | | Алиасы импортов | Как настроить алиасы импортов? |
| Biome | Как настроить линтер и форматтер? | | Biome | Как настроить линтер и форматтер? |
| PostCSS | Какие плагины PostCSS нужны и как их настроить? | | PostCSS | Какие плагины PostCSS нужны и как их настроить? |
@@ -73,6 +66,8 @@ description: Стандарты разработки фронтенд-прило
| Структура проекта | Как организованы папки и файлы по SLM? | | Структура проекта | Как организованы папки и файлы по SLM? |
| Компоненты | Как устроен компонент: файлы, пропсы, clsx? | | Компоненты | Как устроен компонент: файлы, пропсы, clsx? |
| Файлы роутинга | Как описывать layout, page, loading, error, not-found? | | Файлы роутинга | Как описывать layout, page, loading, error, not-found? |
| REST-клиент | Как настроить клиент внешнего REST API? |
| Получение данных | Как выбрать способ получения данных под рендер страницы? |
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? | | Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
| Стили | Как писать CSS: вложенность, медиа, токены? | | Стили | Как писать CSS: вложенность, медиа, токены? |
| SVG-спрайты | Как использовать SVG-спрайты в коде? | | SVG-спрайты | Как использовать SVG-спрайты в коде? |
@@ -82,16 +77,3 @@ description: Стандарты разработки фронтенд-прило
| Хуки | _(не заполнен)_ | | Хуки | _(не заполнен)_ |
| Шрифты | _(не заполнен)_ | | Шрифты | _(не заполнен)_ |
| Локализация | _(не заполнен)_ | | Локализация | _(не заполнен)_ |
### Данные
**Как работать с источниками данных** — REST, realtime и потребление в компонентах.
| Раздел | Отвечает на вопрос |
|--------|-------------------|
| Источники данных | Как устроена работа с данными в проекте? |
| REST: Автоматическая генерация | Как сгенерировать REST-клиент автоматически из OpenAPI? |
| REST: Ручное создание | Как написать REST-клиент вручную? |
| REST: Серверные компоненты | Как получать данные в серверных компонентах? |
| REST: Клиентские компоненты | Как получать данные в клиентских компонентах? |
| Realtime | Как работать с realtime-каналами и сокетами? |