docs: обновить слой infra в документации
Some checks failed
CI/CD Pipeline / docker (push) Failing after 50s
CI/CD Pipeline / deploy (push) Has been skipped

- обновлены упоминания слоя infrastructure на infra
- добавлены метаданные страниц архитектуры
- исправлены ссылки между разделами архитектуры
This commit is contained in:
2026-05-08 06:57:37 +03:00
parent bf1781f143
commit 12c8192d85
24 changed files with 110 additions and 94 deletions

View File

@@ -63,4 +63,4 @@
- [Biome](./applied/biome.md) — Установка и настройка линтера-форматтера в новом проекте. - [Biome](./applied/biome.md) — Установка и настройка линтера-форматтера в новом проекте.
- [PostCSS](./applied/postcss.md) — Установка и настройка CSS-процессора в новом проекте. - [PostCSS](./applied/postcss.md) — Установка и настройка CSS-процессора в новом проекте.
- [VS Code](./applied/vscode.md) — Единые настройки редактора и расширений для команды. - [VS Code](./applied/vscode.md) — Единые настройки редактора и расширений для команды.
- [Локализация](./applied/localization.md) — Как организовать локализацию как infrastructure-модуль. - [Локализация](./applied/localization.md) — Как организовать локализацию как infra-модуль.

View File

@@ -1,7 +1,7 @@
--- ---
title: Алиасы импортов title: Алиасы импортов
description: Какие алиасы импортов есть в проекте и как ими пользоваться. description: Какие алиасы импортов есть в проекте и как ими пользоваться.
keywords: [алиасы, aliases, paths, tsconfig, импорты, baseUrl, app, layouts, screens, widgets, business, infrastructure, ui, shared] keywords: [алиасы, aliases, paths, tsconfig, импорты, baseUrl, app, layouts, screens, widgets, business, infra, ui, shared]
--- ---
# Алиасы импортов # Алиасы импортов
@@ -21,7 +21,7 @@ keywords: [алиасы, aliases, paths, tsconfig, импорты, baseUrl, app,
"screens/*": ["./src/screens/*"], "screens/*": ["./src/screens/*"],
"widgets/*": ["./src/widgets/*"], "widgets/*": ["./src/widgets/*"],
"business/*": ["./src/business/*"], "business/*": ["./src/business/*"],
"infrastructure/*": ["./src/infrastructure/*"], "infra/*": ["./src/infra/*"],
"ui/*": ["./src/ui/*"], "ui/*": ["./src/ui/*"],
"shared/*": ["./src/shared/*"] "shared/*": ["./src/shared/*"]
} }

View File

@@ -1,22 +1,22 @@
--- ---
title: Локализация title: Локализация
description: Как организовать локализацию как infrastructure-модуль. description: Как организовать локализацию как infra-модуль.
--- ---
# Локализация # Локализация
Как организовать локализацию как infrastructure-модуль. Как организовать локализацию как infra-модуль.
## Назначение ## Назначение
Локализация — инфраструктурная подсистема приложения. Она отвечает за текущую локаль, словари, форматирование переводов и API для компонентов. Локализация — инфраструктурная подсистема приложения. Она отвечает за текущую локаль, словари, форматирование переводов и API для компонентов.
Код локализации живёт в `src/infrastructure/i18n/`. Компоненты и модули не читают словари напрямую — они используют публичный API infrastructure-модуля. Код локализации живёт в `src/infra/i18n/`. Компоненты и модули не читают словари напрямую — они используют публичный API infra-модуля.
## Структура ## Структура
```text ```text
src/infrastructure/i18n/ src/infra/i18n/
├── config/ ├── config/
│ └── i18n.config.ts │ └── i18n.config.ts
├── dictionaries/ ├── dictionaries/
@@ -31,16 +31,16 @@ src/infrastructure/i18n/
└── index.ts └── index.ts
``` ```
Набор сегментов может отличаться, но публичная точка входа остаётся одна — `infrastructure/i18n`. Набор сегментов может отличаться, но публичная точка входа остаётся одна — `infra/i18n`.
## Подключение ## Подключение
`app/` только подключает готовый провайдер локализации. Реализация провайдера, словари и конфиг остаются в `infrastructure/i18n/`. `app/` только подключает готовый провайдер локализации. Реализация провайдера, словари и конфиг остаются в `infra/i18n/`.
```tsx ```tsx
// src/app/layout.tsx // src/app/layout.tsx
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { I18nProvider } from 'infrastructure/i18n' import { I18nProvider } from 'infra/i18n'
type RootLayoutProps = { type RootLayoutProps = {
children: ReactNode children: ReactNode
@@ -62,7 +62,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
Компоненты получают переводы через готовый API модуля локализации: Компоненты получают переводы через готовый API модуля локализации:
```tsx ```tsx
import { useTranslation } from 'infrastructure/i18n' import { useTranslation } from 'infra/i18n'
export const ProfileTitle = () => { export const ProfileTitle = () => {
const { t } = useTranslation() const { t } = useTranslation()
@@ -73,9 +73,9 @@ export const ProfileTitle = () => {
## Правила ## Правила
- Локализация живёт в `infrastructure/i18n/`. - Локализация живёт в `infra/i18n/`.
- `app/` только подключает готовый provider и передаёт locale. - `app/` только подключает готовый provider и передаёт locale.
- Словари не импортируются напрямую в компоненты, screens или business-модули. - Словари не импортируются напрямую в компоненты, screens или business-модули.
- Ключи переводов не собираются динамически из строк, если это ломает типизацию и поиск. - Ключи переводов не собираются динамически из строк, если это ломает типизацию и поиск.
- Тексты интерфейса не хардкодятся в переиспользуемых компонентах, если они должны переводиться. - Тексты интерфейса не хардкодятся в переиспользуемых компонентах, если они должны переводиться.
- Форматирование дат, чисел и валют должно проходить через API локализации или отдельные утилиты infrastructure-модуля. - Форматирование дат, чисел и валют должно проходить через API локализации или отдельные утилиты infra-модуля.

View File

@@ -25,7 +25,7 @@ description: Как работать со страницами и другими
| Прогрев SWR-кеша, начальное состояние, подключение провайдеров | `src/app/**`, только через готовые обёртки из нижних слоёв | | Прогрев SWR-кеша, начальное состояние, подключение провайдеров | `src/app/**`, только через готовые обёртки из нижних слоёв |
| UI страницы | `screens/` | | UI страницы | `screens/` |
| Каркас страницы: header, footer, sidebar | `layouts/` | | Каркас страницы: header, footer, sidebar | `layouts/` |
| Провайдеры, сторы, хуки, API-клиенты, сервисы | нижние слои (`screens/`, `business/`, `infrastructure/`, `shared/`) | | Провайдеры, сторы, хуки, API-клиенты, сервисы | нижние слои (`screens/`, `business/`, `infra/`, `shared/`) |
| CSS Modules и стили компонентов | рядом с компонентами, не в `src/app/**` | | CSS Modules и стили компонентов | рядом с компонентами, не в `src/app/**` |
## Что можно делать в `page.tsx` ## Что можно делать в `page.tsx`
@@ -79,7 +79,7 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
```tsx ```tsx
import { notFound } from 'next/navigation' import { notFound } from 'next/navigation'
import { userApi } from 'infrastructure/backend-api' import { userApi } from 'infra/backend-api'
import { UserScreen } from 'screens/user' import { UserScreen } from 'screens/user'
type UserPageProps = { type UserPageProps = {
@@ -109,7 +109,7 @@ import {
backendApi, backendApi,
getCurrentUserKey, getCurrentUserKey,
getPostListKey, getPostListKey,
} from 'infrastructure/backend-api' } from 'infra/backend-api'
type FeedLayoutProps = { type FeedLayoutProps = {
children: ReactNode children: ReactNode

View File

@@ -46,7 +46,7 @@ src/
├── screens/ # Контент конкретной страницы ├── screens/ # Контент конкретной страницы
├── widgets/ # Составные блоки интерфейса, не привязанные к домену ├── widgets/ # Составные блоки интерфейса, не привязанные к домену
├── business/ # Бизнес-домены (auth, catalog, orders) ├── business/ # Бизнес-домены (auth, catalog, orders)
├── infrastructure/ # Техсервисы (theme, i18n, API-адаптеры) ├── infra/ # Техсервисы (theme, i18n, API-адаптеры)
├── ui/ # UI-кит без бизнес-логики ├── ui/ # UI-кит без бизнес-логики
└── shared/ # Общие ресурсы (утилиты, типы, стили) └── shared/ # Общие ресурсы (утилиты, типы, стили)
``` ```

View File

@@ -1,17 +1,18 @@
---
title: SLM Design
description: Назначение архитектуры, ключевые принципы и карта разделов документации
---
# SLM Design # SLM Design
Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили. Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили.
::: warning Локальная копия
Документация по архитектуре — локальная копия. Оригинал находится на сайте [slm-design.gromlab.ru](https://slm-design.gromlab.ru/).
:::
## Разделы спецификации ## Разделы спецификации
Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше: Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше:
- [Слои](./layers.md) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя. - [Слои](/architecture/layers) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя.
- [Модули](./modules.md) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента. - [Модули](/architecture/modules) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента.
- [Сегменты](./segments.md) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов. - [Сегменты](/architecture/segments) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов.
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты. Рекомендуемый порядок чтения: обзор → слои → модули → сегменты.

View File

@@ -1,3 +1,8 @@
---
title: Слои
description: Иерархия слоёв от app до shared, правила зависимостей и зона ответственности каждого слоя
---
# Слои # Слои
Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом. Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом.
@@ -246,4 +251,4 @@ src/shared/
### Требования ### Требования
- Не имеет runtime-состояния - Не имеет runtime-состояния

View File

@@ -1,3 +1,8 @@
---
title: Модули
description: Структура модуля, типы (UI, бизнес, инфра), публичный API, отличие модуля от компонента
---
# Модули # Модули
Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом. Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом.
@@ -146,7 +151,7 @@ backend-api/
└── index.ts # публичный API └── index.ts # публичный API
``` ```
Подробное описание сегментов — в разделе [Сегменты](./segments.md). Подробное описание сегментов — в разделе [Сегменты](/architecture/segments).
## Публичный API ## Публичный API
@@ -281,4 +286,4 @@ export const HomeScreen = () => {
- блок с данными/логикой → `widgets/` - блок с данными/логикой → `widgets/`
- представление бизнес-домена → `business/{area}/parts/` - представление бизнес-домена → `business/{area}/parts/`
Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность. Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность.

View File

@@ -1,3 +1,8 @@
---
title: Сегменты
description: Сегменты внутри модуля (ui/, model/, lib/ и др.), назначение и правила размещения файлов
---
# Сегменты # Сегменты
Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит. Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит.
@@ -37,7 +42,7 @@
- Не получает данные самостоятельно, не выбирает источник данных и не композирует данные. - Не получает данные самостоятельно, не выбирает источник данных и не композирует данные.
- Не содержит бизнес-логику или сценарную логику. - Не содержит бизнес-логику или сценарную логику.
Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](./modules.md#компонент). Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](/architecture/modules#компонент).
Корневой файл модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`. Корневой файл модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`.
@@ -173,4 +178,4 @@ lib/
config/ config/
├── routes.ts ├── routes.ts
└── constants.ts └── constants.ts
``` ```

View File

@@ -25,7 +25,7 @@ keywords: [создать проект, новый проект, с нуля, in
## Канон раскладки ## Канон раскладки
В `src/` допустимы только слои SLM: `app/`, `layouts/`, `screens/`, `widgets/`, `business/`, `infrastructure/`, `ui/`, `shared/`. Любая другая папка в `src/` — нарушение канона ([Структура проекта](/docs/applied/project-structure), [Архитектура](/docs/basics/architecture/)). В `src/` допустимы только слои SLM: `app/`, `layouts/`, `screens/`, `widgets/`, `business/`, `infra/`, `ui/`, `shared/`. Любая другая папка в `src/` — нарушение канона ([Структура проекта](/docs/applied/project-structure), [Архитектура](/docs/basics/architecture/)).
В частности: `src/app/` содержит только файлы роутинга Next.js и инициализации, без каталогов `styles/`, `assets/`, `components/`. В частности: `src/app/` содержит только файлы роутинга Next.js и инициализации, без каталогов `styles/`, `assets/`, `components/`.
@@ -85,6 +85,6 @@ CSS-процессор поверх базовых стилей: `@custom-media`
- **Порядок шагов фиксирован.** Перестановка ломает зависимости (PostCSS требует базовых стилей, VS Code — установленного Biome). - **Порядок шагов фиксирован.** Перестановка ломает зависимости (PostCSS требует базовых стилей, VS Code — установленного Biome).
- **Между шагами обязательна проверка** из соответствующего раздела. Не переходить дальше, пока чеклист текущего шага не пройден. - **Между шагами обязательна проверка** из соответствующего раздела. Не переходить дальше, пока чеклист текущего шага не пройден.
- **Слои `src/`** (`layouts/`, `screens/`, `widgets/`, `business/`, `infrastructure/`, `ui/`) не создавать авансом. Появляются по мере первого модуля. Исключения — `src/app/` (создаётся `create-next-app`), `src/shared/styles/` (шаг 1) и `src/shared/sprites/icons/` (шаг 6). - **Слои `src/`** (`layouts/`, `screens/`, `widgets/`, `business/`, `infra/`, `ui/`) не создавать авансом. Появляются по мере первого модуля. Исключения — `src/app/` (создаётся `create-next-app`), `src/shared/styles/` (шаг 1) и `src/shared/sprites/icons/` (шаг 6).
- **Посторонние каталоги в `src/`** (`assets/`, `utils/`, `lib/`, `components/` и т.п.) — запрещены. - **Посторонние каталоги в `src/`** (`assets/`, `utils/`, `lib/`, `components/` и т.п.) — запрещены.
- **Подмножество шагов допустимо.** Можно ставить только Next.js и часть инструментов; полный набор — это эталон, а не обязательство. - **Подмножество шагов допустимо.** Можно ставить только Next.js и часть инструментов; полный набор — это эталон, а не обязательство.

View File

@@ -89,7 +89,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
mkdir -p src/shared/styles mkdir -p src/shared/styles
``` ```
Остальные слои (`layouts/`, `screens/`, `widgets/`, `business/`, `infrastructure/`, `ui/`) заводятся при появлении первого модуля в них. `src/shared/styles/` — единственный подкаталог `shared/`, который заводится сразу: без него не настроить стили на следующих шагах. Остальные слои (`layouts/`, `screens/`, `widgets/`, `business/`, `infra/`, `ui/`) заводятся при появлении первого модуля в них. `src/shared/styles/` — единственный подкаталог `shared/`, который заводится сразу: без него не настроить стили на следующих шагах.
## Правила ## Правила

View File

@@ -1,7 +1,7 @@
--- ---
title: Источники данных title: Источники данных
description: Какие источники данных используются в проекте и как с ними работать. description: Какие источники данных используются в проекте и как с ними работать.
keywords: [данные, api, rest, realtime, клиент, swr, infrastructure, введение, карта раздела] keywords: [данные, api, rest, realtime, клиент, swr, infra, введение, карта раздела]
--- ---
# Источники данных # Источники данных
@@ -10,7 +10,7 @@ keywords: [данные, api, rest, realtime, клиент, swr, infrastructure,
## Принципы раздела ## Принципы раздела
- **Клиент — в `infrastructure/`.** Каждый внешний сервис — отдельный модуль слоя `infrastructure/{service-name}/`. - **Клиент — в `infra/`.** Каждый внешний сервис — отдельный модуль слоя `infra/{service-name}/`.
- **Прямой `fetch` запрещён.** Запросы идут только через клиент модуля. Исключения — точечные и обоснованные. - **Прямой `fetch` запрещён.** Запросы идут только через клиент модуля. Исключения — точечные и обоснованные.
- **Источник данных диктует канал.** REST, realtime и т.п. — независимые подразделы, у каждого своя модель клиента и своё потребление. - **Источник данных диктует канал.** REST, realtime и т.п. — независимые подразделы, у каждого своя модель клиента и своё потребление.
- **Серверные и клиентские компоненты потребляют по-разному.** Server Components — прямой `await` метода клиента, клиентские — через готовые GET-хуки REST-клиента (`useGetUserList`, `useGetPostDetail` и т.п.). SWR инкапсулирован в хуке, компонент про него не знает. - **Серверные и клиентские компоненты потребляют по-разному.** Server Components — прямой `await` метода клиента, клиентские — через готовые GET-хуки REST-клиента (`useGetUserList`, `useGetPostDetail` и т.п.). SWR инкапсулирован в хуке, компонент про него не знает.
@@ -40,7 +40,7 @@ keywords: [данные, api, rest, realtime, клиент, swr, infrastructure,
Канал push-данных: WebSocket, SSE, событийные шины. Транспорт не зашит в правила — важна абстракция «подписка». Канал push-данных: WebSocket, SSE, событийные шины. Транспорт не зашит в правила — важна абстракция «подписка».
- [Realtime](/docs/data/realtime) — клиент realtime в `infrastructure/`, потребление через `useSWRSubscription` или прямые подписки. - [Realtime](/docs/data/realtime) — клиент realtime в `infra/`, потребление через `useSWRSubscription` или прямые подписки.
## Что даёт раздел ## Что даёт раздел
@@ -48,7 +48,7 @@ keywords: [данные, api, rest, realtime, клиент, swr, infrastructure,
- Где живёт код работы с API и почему именно там. - Где живёт код работы с API и почему именно там.
- Когда генерировать клиент автоматически, а когда писать вручную, и как структурирован каждый из вариантов. - Когда генерировать клиент автоматически, а когда писать вручную, и как структурирован каждый из вариантов.
- Какие GET-хуки относятся к REST-клиенту и почему они живут в `infrastructure/{service-name}/hooks/`. - Какие GET-хуки относятся к REST-клиенту и почему они живут в `infra/{service-name}/hooks/`.
- Как выбрать стратегию получения REST-данных под конкретную ситуацию. - Как выбрать стратегию получения REST-данных под конкретную ситуацию.
- Как подключать realtime-источники в общую модель работы с данными. - Как подключать realtime-источники в общую модель работы с данными.
- Какие правила обязательны и какие отклонения допустимы. - Какие правила обязательны и какие отклонения допустимы.

View File

@@ -10,7 +10,7 @@ keywords: [realtime, websocket, sse, подписка, swr subscription, useSWRS
## Принципы ## Принципы
- **Клиент realtime — в `infrastructure/`** отдельным модулем по имени канала. То же правило, что и для REST: никаких прямых соединений в коде приложения. - **Клиент realtime — в `infra/`** отдельным модулем по имени канала. То же правило, что и для REST: никаких прямых соединений в коде приложения.
- **Подписка — единица потребления.** Клиент даёт функцию `subscribe(topic, handler) → unsubscribe`. Внутри — конкретный транспорт. - **Подписка — единица потребления.** Клиент даёт функцию `subscribe(topic, handler) → unsubscribe`. Внутри — конкретный транспорт.
- **Использование на клиенте — два сценария:** - **Использование на клиенте — два сценария:**
- **`useSWRSubscription`** — для данных, которые показываются в UI и должны кешироваться/синхронизироваться с REST. - **`useSWRSubscription`** — для данных, которые показываются в UI и должны кешироваться/синхронизироваться с REST.
@@ -19,7 +19,7 @@ keywords: [realtime, websocket, sse, подписка, swr subscription, useSWRS
## Размещение клиента ## Размещение клиента
```text ```text
src/infrastructure/ src/infra/
└── {channel-name}/ └── {channel-name}/
├── connection.ts # установление соединения, реконнект ├── connection.ts # установление соединения, реконнект
├── subscribe.ts # subscribe(topic, handler) → unsubscribe ├── subscribe.ts # subscribe(topic, handler) → unsubscribe
@@ -33,7 +33,7 @@ src/infrastructure/
'use client' 'use client'
import useSWRSubscription from 'swr/subscription' import useSWRSubscription from 'swr/subscription'
import { subscribe } from 'infrastructure/notifications' import { subscribe } from 'infra/notifications'
export function NotificationCounter() { export function NotificationCounter() {
const { data: count } = useSWRSubscription( const { data: count } = useSWRSubscription(
@@ -56,7 +56,7 @@ export function NotificationCounter() {
'use client' 'use client'
import { useEffect } from 'react' import { useEffect } from 'react'
import { subscribe } from 'infrastructure/notifications' import { subscribe } from 'infra/notifications'
import { showToast } from 'ui/toast' import { showToast } from 'ui/toast'
export function NotificationsToaster() { export function NotificationsToaster() {
@@ -74,6 +74,6 @@ export function NotificationsToaster() {
## Запрет прямых соединений ## Запрет прямых соединений
Создавать `new WebSocket(...)`, `new EventSource(...)` или подписываться на событийные шины напрямую в коде приложения — запрещено. Все соединения проходят через клиент в `infrastructure/`. Создавать `new WebSocket(...)`, `new EventSource(...)` или подписываться на событийные шины напрямую в коде приложения — запрещено. Все соединения проходят через клиент в `infra/`.
Исключения — точечные и обоснованные (например, диагностический скрипт), помечаются комментарием. Исключения — точечные и обоснованные (например, диагностический скрипт), помечаются комментарием.

View File

@@ -19,7 +19,7 @@ https://petstore3.swagger.io/api/v3/openapi.json
Имена модуля: Имена модуля:
```text ```text
src/infrastructure/pet-store-api/ src/infra/pet-store-api/
petStoreApi petStoreApi
pet-store-api.generated.ts pet-store-api.generated.ts
``` ```
@@ -31,7 +31,7 @@ pet-store-api.generated.ts
```json ```json
{ {
"scripts": { "scripts": {
"codegen:pet-store-api": "npx @gromlab/api-codegen@latest -i https://petstore3.swagger.io/api/v3/openapi.json -o src/infrastructure/pet-store-api/generated -n pet-store-api.generated" "codegen:pet-store-api": "npx @gromlab/api-codegen@latest -i https://petstore3.swagger.io/api/v3/openapi.json -o src/infra/pet-store-api/generated -n pet-store-api.generated"
} }
} }
``` ```
@@ -53,7 +53,7 @@ npm run codegen:pet-store-api
Ожидаемый результат: Ожидаемый результат:
```text ```text
src/infrastructure/pet-store-api/generated/ src/infra/pet-store-api/generated/
└── pet-store-api.generated.ts └── pet-store-api.generated.ts
``` ```
@@ -77,7 +77,7 @@ petStoreApi.pet.getPetById(...)
Сгенерированный код не должен напрямую использоваться из приложения. Сначала создаётся настроенный инстанс клиента. Сгенерированный код не должен напрямую использоваться из приложения. Сначала создаётся настроенный инстанс клиента.
```ts ```ts
// src/infrastructure/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 httpClient = new HttpClient({ const httpClient = new HttpClient({
@@ -102,7 +102,7 @@ export const petStoreApi = new Api(httpClient)
Сгенерированный файл не правится руками. Если OpenAPI-спецификация неполная или генератор дал слишком общий тип (`object`, `unknown`, отсутствующее поле), расширения живут в `types/`. Сгенерированный файл не правится руками. Если OpenAPI-спецификация неполная или генератор дал слишком общий тип (`object`, `unknown`, отсутствующее поле), расширения живут в `types/`.
```text ```text
src/infrastructure/biocad-less-api/ src/infra/biocad-less-api/
├── generated/ ├── generated/
│ └── biocad-less-api.generated.ts │ └── biocad-less-api.generated.ts
├── types/ ├── types/
@@ -115,7 +115,7 @@ src/infrastructure/biocad-less-api/
Пример расширения generated-типа: Пример расширения generated-типа:
```ts ```ts
// src/infrastructure/biocad-less-api/types/term.ts // src/infra/biocad-less-api/types/term.ts
import type { TermRecordItem } from '../generated/biocad-less-api.generated' import type { TermRecordItem } from '../generated/biocad-less-api.generated'
declare module '../generated/biocad-less-api.generated' { declare module '../generated/biocad-less-api.generated' {
@@ -149,7 +149,7 @@ export type TermRecordItemExtended = Omit<
``` ```
```ts ```ts
// src/infrastructure/biocad-less-api/types/index.ts // src/infra/biocad-less-api/types/index.ts
export type { TermRecordItemExtended } from './term' export type { TermRecordItemExtended } from './term'
``` ```
@@ -158,18 +158,18 @@ export type { TermRecordItemExtended } from './term'
## Публичный API ## Публичный API
```ts ```ts
// src/infrastructure/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 { Pet } from './generated/pet-store-api.generated'
export * from './hooks' export * from './hooks'
``` ```
Наружу импортируют только из `infrastructure/pet-store-api`, не из `generated/`. Наружу импортируют только из `infra/pet-store-api`, не из `generated/`.
Если у модуля есть расширенные типы, они тоже реэкспортируются через `index.ts`: Если у модуля есть расширенные типы, они тоже реэкспортируются через `index.ts`:
```ts ```ts
// src/infrastructure/biocad-less-api/index.ts // src/infra/biocad-less-api/index.ts
export type { TermRecordItemExtended } from './types' export type { TermRecordItemExtended } from './types'
``` ```

View File

@@ -1,7 +1,7 @@
--- ---
title: GET-хуки REST-клиента title: GET-хуки REST-клиента
description: Прозрачные SWR-обёртки над GET-методами REST-клиента. description: Прозрачные SWR-обёртки над GET-методами REST-клиента.
keywords: [rest, swr, get-хуки, client components, infrastructure] keywords: [rest, swr, get-хуки, client components, infra]
--- ---
# GET-хуки REST-клиента # GET-хуки REST-клиента
@@ -13,7 +13,7 @@ GET-хуки REST-клиента — прозрачные SWR-обёртки н
GET-хуки принадлежат REST-клиенту конкретного сервиса и живут рядом с ним: GET-хуки принадлежат REST-клиенту конкретного сервиса и живут рядом с ним:
```text ```text
src/infrastructure/ src/infra/
└── pet-store-api/ └── pet-store-api/
├── client.ts ├── client.ts
├── generated/ ├── generated/
@@ -41,7 +41,7 @@ src/infrastructure/
## Пример списка ## Пример списка
```ts ```ts
// src/infrastructure/pet-store-api/hooks/use-get-pet-list.hook.ts // src/infra/pet-store-api/hooks/use-get-pet-list.hook.ts
import useSWR from 'swr' import useSWR from 'swr'
import type { SWRConfiguration } from 'swr' import type { SWRConfiguration } from 'swr'
import { petStoreApi } from '../client' import { petStoreApi } from '../client'
@@ -74,7 +74,7 @@ import { SWRConfig, unstable_serialize } from 'swr'
import { import {
getPetListKey, getPetListKey,
petStoreApi, petStoreApi,
} from 'infrastructure/pet-store-api' } from 'infra/pet-store-api'
export default function PetsLayout({ children }: { children: ReactNode }) { export default function PetsLayout({ children }: { children: ReactNode }) {
const petsPromise = petStoreApi.pet.findPetsByStatus({ status: 'available' }) const petsPromise = petStoreApi.pet.findPetsByStatus({ status: 'available' })
@@ -102,7 +102,7 @@ const { data: pets } = useGetPetList('available')
## Пример detail-запроса ## Пример detail-запроса
```ts ```ts
// src/infrastructure/pet-store-api/hooks/use-get-pet-detail.hook.ts // src/infra/pet-store-api/hooks/use-get-pet-detail.hook.ts
import useSWR from 'swr' import useSWR from 'swr'
import type { SWRConfiguration } from 'swr' import type { SWRConfiguration } from 'swr'
import { petStoreApi } from '../client' import { petStoreApi } from '../client'
@@ -141,23 +141,23 @@ const key = isReady ? getPetDetailKey(id) : null
## Экспорт ## Экспорт
```ts ```ts
// src/infrastructure/pet-store-api/hooks/index.ts // src/infra/pet-store-api/hooks/index.ts
export { getPetListKey, useGetPetList } from './use-get-pet-list.hook' export { getPetListKey, useGetPetList } from './use-get-pet-list.hook'
export type { PetStatus } from './use-get-pet-list.hook' export type { PetStatus } from './use-get-pet-list.hook'
export { getPetDetailKey, useGetPetDetail } from './use-get-pet-detail.hook' export { getPetDetailKey, useGetPetDetail } from './use-get-pet-detail.hook'
``` ```
```ts ```ts
// src/infrastructure/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 { Pet } from './generated/pet-store-api.generated'
export * from './hooks' export * from './hooks'
``` ```
## Где заканчивается infrastructure ## Где заканчивается infra
```ts ```ts
// Хорошо: infrastructure, прозрачный GET-хук // Хорошо: infra, прозрачный GET-хук
const { data: pets } = useGetPetList('available') const { data: pets } = useGetPetList('available')
``` ```
@@ -184,7 +184,7 @@ const { data } = useSWR(
() => petStoreApi.pet.findPetsByStatus({ status }), () => petStoreApi.pet.findPetsByStatus({ status }),
) )
// Плохо — несколько GET внутри infrastructure-хука // Плохо — несколько GET внутри infra-хука
export const usePetDashboard = () => { export const usePetDashboard = () => {
const available = useGetPetList('available') const available = useGetPetList('available')
const sold = useGetPetList('sold') const sold = useGetPetList('sold')

View File

@@ -1,12 +1,12 @@
--- ---
title: Создание клиента title: Создание клиента
description: Из чего состоит REST-клиент и какие части нужно подготовить перед использованием API. description: Из чего состоит REST-клиент и какие части нужно подготовить перед использованием API.
keywords: [rest, клиент, infrastructure, методы, openapi, get-хуки, swr] keywords: [rest, клиент, infra, методы, openapi, get-хуки, swr]
--- ---
# Создание клиента # Создание клиента
REST-клиент — это infrastructure-модуль, через который проект работает с внешним REST API. REST-клиент — это infra-модуль, через который проект работает с внешним REST API.
На этом этапе нужно подготовить клиент сервиса: создать оболочку клиента, получить методы API и добавить GET-хуки для клиентских компонентов. На этом этапе нужно подготовить клиент сервиса: создать оболочку клиента, получить методы API и добавить GET-хуки для клиентских компонентов.
@@ -57,7 +57,7 @@ GET-хуки именуются с префиксом `useGet`: `useGetPetList`,
## Структура модуля ## Структура модуля
```text ```text
src/infrastructure/{service-name}/ src/infra/{service-name}/
├── client.ts # самописная оболочка и инстанс клиента ├── client.ts # самописная оболочка и инстанс клиента
├── generated/ или methods/ # методы API ├── generated/ или methods/ # методы API
├── hooks/ # GET-хуки REST-клиента ├── hooks/ # GET-хуки REST-клиента

View File

@@ -1,7 +1,7 @@
--- ---
title: Ручное создание title: Ручное создание
description: Создание REST-клиента вручную, когда OpenAPI нет или он неполный. description: Создание REST-клиента вручную, когда OpenAPI нет или он неполный.
keywords: [rest, ручной клиент, fetch, methods, dto, errors, infrastructure] keywords: [rest, ручной клиент, fetch, methods, dto, errors, infra]
--- ---
# Ручное создание # Ручное создание
@@ -13,7 +13,7 @@ keywords: [rest, ручной клиент, fetch, methods, dto, errors, infrast
## Что нужно создать ## Что нужно создать
```text ```text
src/infrastructure/ src/infra/
└── pet-project-api/ └── pet-project-api/
├── methods/ ├── methods/
│ └── posts.ts │ └── posts.ts
@@ -43,7 +43,7 @@ src/infrastructure/
DTO запросов и ответов живут в `types/`. `client.ts` не содержит DTO и доменные типы. DTO запросов и ответов живут в `types/`. `client.ts` не содержит DTO и доменные типы.
```ts ```ts
// src/infrastructure/pet-project-api/types/post.ts // src/infra/pet-project-api/types/post.ts
export type PostDto = { export type PostDto = {
id: string id: string
slug: string slug: string
@@ -57,14 +57,14 @@ export type PostListQueryDto = {
``` ```
```ts ```ts
// src/infrastructure/pet-project-api/types/index.ts // src/infra/pet-project-api/types/index.ts
export type { PostDto, PostListQueryDto } from './post' export type { PostDto, PostListQueryDto } from './post'
``` ```
Типы, которые нужны только базовому транспорту, можно держать отдельно: Типы, которые нужны только базовому транспорту, можно держать отдельно:
```ts ```ts
// src/infrastructure/pet-project-api/types/client.ts // src/infra/pet-project-api/types/client.ts
export type QueryParams = Record<string, string | number | boolean> export type QueryParams = Record<string, string | number | boolean>
``` ```
@@ -73,7 +73,7 @@ export type QueryParams = Record<string, string | number | boolean>
Ошибка API тоже относится к REST-модулю. Ошибка API тоже относится к REST-модулю.
```ts ```ts
// src/infrastructure/pet-project-api/errors/pet-project-api.error.ts // src/infra/pet-project-api/errors/pet-project-api.error.ts
export class PetProjectApiError extends Error { export class PetProjectApiError extends Error {
constructor( constructor(
public readonly status: number, public readonly status: number,
@@ -90,7 +90,7 @@ export class PetProjectApiError extends Error {
`client.ts` содержит только транспортную оболочку и сборку инстанса. Прямой `fetch` живёт здесь, а не в компонентах и не в методах верхних слоёв. `client.ts` содержит только транспортную оболочку и сборку инстанса. Прямой `fetch` живёт здесь, а не в компонентах и не в методах верхних слоёв.
```ts ```ts
// src/infrastructure/pet-project-api/client.ts // src/infra/pet-project-api/client.ts
import { PetProjectApiError } from './errors/pet-project-api.error' import { PetProjectApiError } from './errors/pet-project-api.error'
import type { QueryParams } from './types/client' import type { QueryParams } from './types/client'
@@ -131,7 +131,7 @@ export class PetProjectApiClient {
Методы группируются по сущностям в `methods/`. Они не знают про React, SWR и UI. Методы группируются по сущностям в `methods/`. Они не знают про React, SWR и UI.
```ts ```ts
// src/infrastructure/pet-project-api/methods/posts.ts // src/infra/pet-project-api/methods/posts.ts
import type { PetProjectApiClient } from '../client' import type { PetProjectApiClient } from '../client'
import type { PostDto, PostListQueryDto } from '../types/post' import type { PostDto, PostListQueryDto } from '../types/post'
@@ -155,7 +155,7 @@ export function postsMethods(client: PetProjectApiClient) {
`index.ts` собирает именованный API-объект и открывает наружу только публичные части модуля. `index.ts` собирает именованный API-объект и открывает наружу только публичные части модуля.
```ts ```ts
// src/infrastructure/pet-project-api/index.ts // src/infra/pet-project-api/index.ts
import { PetProjectApiClient } from './client' import { PetProjectApiClient } from './client'
import { postsMethods } from './methods/posts' import { postsMethods } from './methods/posts'
@@ -173,7 +173,7 @@ export type { PostDto, PostListQueryDto } from './types'
export * from './hooks' export * from './hooks'
``` ```
Внешний код импортирует только из `infrastructure/pet-project-api`, не из внутренних файлов модуля. Внешний код импортирует только из `infra/pet-project-api`, не из внутренних файлов модуля.
## Правила ## Правила

View File

@@ -1,7 +1,7 @@
--- ---
title: REST title: REST
description: Как правильно работать с REST API в проекте. description: Как правильно работать с REST API в проекте.
keywords: [rest, api, данные, infrastructure, клиент, swr, стратегии] keywords: [rest, api, данные, infra, клиент, swr, стратегии]
--- ---
# REST # REST
@@ -15,7 +15,7 @@ REST в проекте проходит через два главных эта
## 1. Создание клиента ## 1. Создание клиента
На этом этапе внешний API оформляется как модуль слоя `infrastructure/`. На этом этапе внешний API оформляется как модуль слоя `infra/`.
Клиент отвечает за: Клиент отвечает за:

View File

@@ -15,13 +15,13 @@ Business-композиция используется, когда просто
- Нужно преобразовать DTO в доменную модель. - Нужно преобразовать DTO в доменную модель.
- Нужно спрятать бизнес-сценарий за доменным API. - Нужно спрятать бизнес-сценарий за доменным API.
Такая логика не пишется в `infrastructure/`. REST-клиент остаётся прозрачным адаптером к API. Такая логика не пишется в `infra/`. REST-клиент остаётся прозрачным адаптером к API.
## Пример поверх одного GET-хука ## Пример поверх одного GET-хука
```ts ```ts
// src/business/pets/hooks/use-available-pets.hook.ts // src/business/pets/hooks/use-available-pets.hook.ts
import { useGetPetList } from 'infrastructure/pet-store-api' import { useGetPetList } from 'infra/pet-store-api'
/** /**
* Доменный список доступных питомцев. * Доменный список доступных питомцев.
@@ -36,13 +36,13 @@ export const useAvailablePets = () => {
} }
``` ```
`useGetPetList` — infrastructure-хук. `hasPets` — бизнес-интерпретация, поэтому она появляется в `business/pets`. `useGetPetList` — infra-хук. `hasPets` — бизнес-интерпретация, поэтому она появляется в `business/pets`.
## Пример композиции нескольких GET-хуков ## Пример композиции нескольких GET-хуков
```ts ```ts
// src/business/pets/hooks/use-pets-dashboard.hook.ts // src/business/pets/hooks/use-pets-dashboard.hook.ts
import { useGetPetList } from 'infrastructure/pet-store-api' import { useGetPetList } from 'infra/pet-store-api'
/** /**
* Данные dashboard по питомцам. * Данные dashboard по питомцам.
@@ -64,13 +64,13 @@ export const usePetsDashboard = () => {
} }
``` ```
Композиция нескольких запросов не добавляется в `infrastructure/pet-store-api/hooks/`, потому что это уже сценарий потребления данных. Композиция нескольких запросов не добавляется в `infra/pet-store-api/hooks/`, потому что это уже сценарий потребления данных.
## Пример auth-состояния ## Пример auth-состояния
```ts ```ts
// src/business/auth/hooks/use-auth-state.hook.ts // src/business/auth/hooks/use-auth-state.hook.ts
import { useGetCurrentUser } from 'infrastructure/backend-api' import { useGetCurrentUser } from 'infra/backend-api'
/** /**
* Состояние авторизации текущего пользователя. * Состояние авторизации текущего пользователя.
@@ -107,7 +107,7 @@ src/business/
## Что запрещено ## Что запрещено
```ts ```ts
// Плохо — business-смысл внутри infrastructure-хука // Плохо — business-смысл внутри infra-хука
export const useGetPetList = (status: PetStatus) => { export const useGetPetList = (status: PetStatus) => {
const query = useSWR(...) const query = useSWR(...)

View File

@@ -21,8 +21,8 @@ keywords: [rest, client components, swr, get-хук, client state]
'use client' 'use client'
import { useState } from 'react' import { useState } from 'react'
import { useGetPetList } from 'infrastructure/pet-store-api' import { useGetPetList } from 'infra/pet-store-api'
import type { PetStatus } from 'infrastructure/pet-store-api' import type { PetStatus } from 'infra/pet-store-api'
const statuses: PetStatus[] = ['available', 'pending', 'sold'] const statuses: PetStatus[] = ['available', 'pending', 'sold']
@@ -60,7 +60,7 @@ export function PetTabs() {
Хук добавляется в REST-модуль сервиса: Хук добавляется в REST-модуль сервиса:
```text ```text
src/infrastructure/pet-store-api/hooks/use-get-pet-list.hook.ts src/infra/pet-store-api/hooks/use-get-pet-list.hook.ts
``` ```
Не создавайте локальный `useSWR` в компоненте. Не создавайте локальный `useSWR` в компоненте.

View File

@@ -30,7 +30,7 @@ keywords: [rest, swr, fallback, initial data, client hooks, unstable_serialize,
## Ключ хука ## Ключ хука
```ts ```ts
// src/infrastructure/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 = (status: PetStatus) =>
['pet-store-api', 'pet', 'list', status] as const ['pet-store-api', 'pet', 'list', status] as const
``` ```
@@ -46,7 +46,7 @@ import { SWRConfig, unstable_serialize } from 'swr'
import { import {
getPetListKey, getPetListKey,
petStoreApi, petStoreApi,
} from 'infrastructure/pet-store-api' } from 'infra/pet-store-api'
type PetsLayoutProps = { type PetsLayoutProps = {
children: ReactNode children: ReactNode
@@ -78,7 +78,7 @@ export default async function PetsLayout({ children }: PetsLayoutProps) {
```tsx ```tsx
'use client' 'use client'
import { useGetPetList } from 'infrastructure/pet-store-api' import { useGetPetList } from 'infra/pet-store-api'
export function PetList() { export function PetList() {
const { data: pets, isLoading } = useGetPetList('available') const { data: pets, isLoading } = useGetPetList('available')

View File

@@ -17,7 +17,7 @@ keywords: [rest, promise.all, параллельные запросы, server co
## Хорошо ## Хорошо
```tsx ```tsx
import { petStoreApi } from 'infrastructure/pet-store-api' import { petStoreApi } 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() {

View File

@@ -19,10 +19,10 @@ 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 'infrastructure/pet-store-api' import { petStoreApi } 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 'infrastructure/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: 'available' })

View File

@@ -29,7 +29,7 @@ SSR/dynamic rendering нужен, когда данные зависят от т
```tsx ```tsx
// src/app/(routes)/pets/page.tsx // src/app/(routes)/pets/page.tsx
import { petStoreApi } from 'infrastructure/pet-store-api' import { petStoreApi } 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() {
@@ -48,7 +48,7 @@ export default async function PetsPage() {
```tsx ```tsx
// src/app/(routes)/pets/[id]/page.tsx // src/app/(routes)/pets/[id]/page.tsx
import { notFound } from 'next/navigation' import { notFound } from 'next/navigation'
import { petStoreApi } from 'infrastructure/pet-store-api' import { petStoreApi } from 'infra/pet-store-api'
import { PetDetailScreen } from 'screens/pet-detail' import { PetDetailScreen } from 'screens/pet-detail'
type PetPageProps = { type PetPageProps = {