docs: добавить стайлгайд nextjs-style-guide в репозиторий

- Добавлена документация SLM-архитектуры, базовых правил и прикладных разделов
- Добавлены разделы: стили, SVG-спрайты, шаблоны генерации, PostCSS, REST, Realtime
- Удалены устаревшие файлы (спрайты, скрипты, стили из app/)
This commit is contained in:
2026-04-30 19:32:10 +03:00
parent bf792f6159
commit f2358da397
60 changed files with 5308 additions and 372 deletions

View File

@@ -0,0 +1,193 @@
---
title: Автогенерация из OpenAPI
description: Генерация REST-клиента из OpenAPI-спецификации через @gromlab/api-codegen.
keywords: [rest, openapi, api-codegen, автогенерация, generated, npx]
---
# Автогенерация из OpenAPI
Автогенерация используется, когда у API есть актуальная OpenAPI-спецификация. Генератор создаёт TypeScript-клиент, типы и методы API, а разработчик вручную добавляет настройку клиента и GET-хуки.
## Пример API
В примерах используется Swagger Petstore:
```text
https://petstore3.swagger.io/api/v3/openapi.json
```
Имена модуля:
```text
src/infrastructure/pet-store-api/
petStoreApi
pet-store-api.generated.ts
```
## Скрипт генерации
`@gromlab/api-codegen` не устанавливается в `devDependencies`. Используем `npx @gromlab/api-codegen@latest`, чтобы запускать свежую версию.
```json
{
"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"
}
}
```
Параметры:
- `-i` — путь к OpenAPI-спецификации: URL или локальный файл.
- `-o` — директория для сгенерированного файла.
- `-n` — имя сгенерированного файла без `.ts`.
Ключ `--swr` не используется. GET-хуки REST-клиента пишутся вручную, чтобы сохранить проектный контракт: один GET-хук = один GET-метод, без бизнес-логики и композиции.
## Генерация
```bash
npm run codegen:pet-store-api
```
Ожидаемый результат:
```text
src/infrastructure/pet-store-api/generated/
└── pet-store-api.generated.ts
```
Сгенерированный файл не правится руками и коммитится в репозиторий.
## Проверка методов
После генерации откройте `generated/pet-store-api.generated.ts` и проверьте фактические имена методов.
Для Petstore нужны GET-операции вида:
```ts
petStoreApi.pet.findPetsByStatus(...)
petStoreApi.pet.getPetById(...)
```
Точные сигнатуры зависят от OpenAPI-схемы и версии генератора. В рабочих задачах всегда сверяйтесь с generated-файлом.
## `client.ts`
Сгенерированный код не должен напрямую использоваться из приложения. Сначала создаётся настроенный инстанс клиента.
```ts
// src/infrastructure/pet-store-api/client.ts
import { Api, HttpClient } from './generated/pet-store-api.generated'
const httpClient = new HttpClient({
baseUrl: 'https://petstore3.swagger.io/api/v3',
baseApiParams: {
secure: false,
headers: {
'Content-Type': 'application/json',
},
},
})
export const petStoreApi = new Api(httpClient)
```
В реальном проекте вместо строки `baseUrl` обычно берётся из env-переменной, нормализуется в `client.ts` и передаётся в `HttpClient` через конфиг.
`client.ts` не содержит расширения типов, `declare module` и `Extended`-типы. Он только настраивает транспорт и экспортирует инстанс клиента.
## Расширение сгенерированных типов
Сгенерированный файл не правится руками. Если OpenAPI-спецификация неполная или генератор дал слишком общий тип (`object`, `unknown`, отсутствующее поле), расширения живут в `types/`.
```text
src/infrastructure/biocad-less-api/
├── generated/
│ └── biocad-less-api.generated.ts
├── types/
│ ├── term.ts
│ └── index.ts
├── client.ts
└── index.ts
```
Пример расширения generated-типа:
```ts
// src/infrastructure/biocad-less-api/types/term.ts
import type { TermRecordItem } from '../generated/biocad-less-api.generated'
declare module '../generated/biocad-less-api.generated' {
interface TermRecordItem {
media?: {
file?: string
title?: string
url?: string
}
}
}
export type TermRecordItemExtended = Omit<
TermRecordItem,
'categories' | 'tags' | 'fields'
> & {
categories?: Array<{
_id?: string
id?: string
slug?: string
name?: string
}>
tags?: Array<{
_id?: string
id?: string
slug?: string
name?: string
}>
fields?: Record<string, unknown>
}
```
```ts
// src/infrastructure/biocad-less-api/types/index.ts
export type { TermRecordItemExtended } from './term'
```
`declare module` используется для добавления отсутствующих полей в generated-интерфейс. `Extended`-тип используется, когда нужно переопределить неточные поля, не трогая generated-файл.
## Публичный API
```ts
// src/infrastructure/pet-store-api/index.ts
export { petStoreApi } from './client'
export type { Pet } from './generated/pet-store-api.generated'
export * from './hooks'
```
Наружу импортируют только из `infrastructure/pet-store-api`, не из `generated/`.
Если у модуля есть расширенные типы, они тоже реэкспортируются через `index.ts`:
```ts
// src/infrastructure/biocad-less-api/index.ts
export type { TermRecordItemExtended } from './types'
```
## Регенерация
При изменении OpenAPI-схемы:
```bash
npm run codegen:pet-store-api
```
Что меняется:
- `generated/pet-store-api.generated.ts` — перезаписывается генератором.
- `client.ts`, `hooks/`, `types/`, `index.ts` — не трогаются автоматически.
Если после регенерации поменялись сигнатуры методов или типы, это исправляется в ручном коде модуля.
## Следующий шаг
После генерации и настройки `client.ts` проверьте серверный вызов метода клиента или добавьте [GET-хук REST-клиента](./hooks.md) для Client Components.

View File

@@ -0,0 +1,206 @@
---
title: GET-хуки REST-клиента
description: Прозрачные SWR-обёртки над GET-методами REST-клиента.
keywords: [rest, swr, get-хуки, client components, infrastructure]
---
# GET-хуки REST-клиента
GET-хуки REST-клиента — прозрачные SWR-обёртки над GET-методами API-клиента. Они нужны, чтобы Client Components получали данные с кешированием, дедупликацией и ревалидацией, не работая с `useSWR` напрямую.
## Где лежат
GET-хуки принадлежат REST-клиенту конкретного сервиса и живут рядом с ним:
```text
src/infrastructure/
└── 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/infrastructure/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 'infrastructure/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/infrastructure/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/infrastructure/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/infrastructure/pet-store-api/index.ts
export { petStoreApi } from './client'
export type { Pet } from './generated/pet-store-api.generated'
export * from './hooks'
```
## Где заканчивается infrastructure
```ts
// Хорошо: infrastructure, прозрачный 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 внутри infrastructure-хука
export const usePetDashboard = () => {
const available = useGetPetList('available')
const sold = useGetPetList('sold')
return { available, sold }
}
// Плохо — бизнес-флаг внутри GET-хука REST-клиента
export const useGetPetList = (status: PetStatus) => {
const query = useSWR(...)
return {
...query,
hasPets: Boolean(query.data?.length),
}
}
```
Подробное потребление таких хуков описано в стратегии [Клиентский GET-хук](../strategies/client-get-hook.md).

View File

@@ -0,0 +1,75 @@
---
title: Создание клиента
description: Из чего состоит REST-клиент и какие части нужно подготовить перед использованием API.
keywords: [rest, клиент, infrastructure, методы, openapi, get-хуки, swr]
---
# Создание клиента
REST-клиент — это infrastructure-модуль, через который проект работает с внешним REST API.
На этом этапе нужно подготовить клиент сервиса: создать оболочку клиента, получить методы API и добавить GET-хуки для клиентских компонентов.
## Из чего состоит клиент
REST-клиент состоит из трёх основных частей:
1. **Клиент** — самописная оболочка над транспортом.
2. **Методы** — сгенерированные из OpenAPI или написанные вручную вызовы API.
3. **GET-хуки** — SWR-обёртки для GET-запросов.
Эти части живут в одном REST-модуле, потому что относятся к одному внешнему сервису.
## Клиент
Клиент — ручной слой, который настраивает работу с API: `baseUrl`, заголовки, авторизацию, обработку ошибок и создание инстанса сервиса.
Даже если методы генерируются из OpenAPI, `client.ts` остаётся ручным файлом проекта.
`client.ts` — только сборочная точка клиента. В нём не размещаются DTO, `declare module`, `Extended`-типы, GET-хуки и бизнес-логика.
## Методы
Методы описывают конкретные запросы к API.
Они появляются одним из двух способов:
- генерируются из OpenAPI в `generated/`;
- создаются вручную в `methods/`.
Подробности:
- [Автогенерация из OpenAPI](./auto.md)
- [Ручное создание](./manual.md)
## GET-хуки
Для GET-запросов добавляются GET-хуки REST-клиента.
Это прозрачные SWR-обёртки над GET-методами клиента. Они живут в `hooks/` этого же REST-модуля и нужны для использования данных в Client Components.
GET-хуки именуются с префиксом `useGet`: `useGetPetList`, `useGetPetDetail`, `useGetCurrentUser`.
Подробности:
- [GET-хуки REST-клиента](./hooks.md)
## Структура модуля
```text
src/infrastructure/{service-name}/
├── client.ts # самописная оболочка и инстанс клиента
├── generated/ или methods/ # методы API
├── hooks/ # GET-хуки REST-клиента
├── types/ # DTO, типы API и расширения типов
├── errors/ # ошибки API, если нужны
└── index.ts # публичный API
```
`index.ts` — единственная точка входа в REST-модуль для внешнего кода.
## Что делаем дальше
1. Создайте методы клиента: [Автогенерация из OpenAPI](./auto.md) или [Ручное создание](./manual.md).
2. Добавьте GET-хуки для GET-запросов: [GET-хуки REST-клиента](./hooks.md).
3. После создания клиента переходите к [Стратегиям получения данных](../strategies/index.md).

View File

@@ -0,0 +1,187 @@
---
title: Ручное создание
description: Создание REST-клиента вручную, когда OpenAPI нет или он неполный.
keywords: [rest, ручной клиент, fetch, methods, dto, errors, infrastructure]
---
# Ручное создание
Ручной REST-клиент используется, когда у API нет OpenAPI-спецификации или она недостаточно точная для автогенерации.
Задача ручного клиента — дать такую же точку входа, как у автогенерированного клиента: именованный API-объект, методы по сущностям, DTO и GET-хуки рядом с клиентом.
## Что нужно создать
```text
src/infrastructure/
└── pet-project-api/
├── methods/
│ └── posts.ts
├── hooks/
│ └── index.ts
├── types/
│ ├── client.ts
│ ├── post.ts
│ └── index.ts
├── errors/
│ └── pet-project-api.error.ts
├── client.ts
└── index.ts
```
| Файл | Роль |
|------|------|
| `client.ts` | Базовый транспорт и создание инстанса клиента |
| `methods/` | Методы API по сущностям |
| `types/` | DTO запросов, ответов и типы клиента |
| `errors/` | Ошибки конкретного API |
| `hooks/` | GET-хуки REST-клиента, если данные нужны в Client Components |
| `index.ts` | Публичный API REST-модуля |
## DTO и типы API
DTO запросов и ответов живут в `types/`. `client.ts` не содержит DTO и доменные типы.
```ts
// src/infrastructure/pet-project-api/types/post.ts
export type PostDto = {
id: string
slug: string
title: string
}
export type PostListQueryDto = {
limit?: number
category?: string
}
```
```ts
// src/infrastructure/pet-project-api/types/index.ts
export type { PostDto, PostListQueryDto } from './post'
```
Типы, которые нужны только базовому транспорту, можно держать отдельно:
```ts
// src/infrastructure/pet-project-api/types/client.ts
export type QueryParams = Record<string, string | number | boolean>
```
## Ошибка API
Ошибка API тоже относится к REST-модулю.
```ts
// src/infrastructure/pet-project-api/errors/pet-project-api.error.ts
export class PetProjectApiError extends Error {
constructor(
public readonly status: number,
message: string,
) {
super(message)
this.name = 'PetProjectApiError'
}
}
```
## Базовый клиент
`client.ts` содержит только транспортную оболочку и сборку инстанса. Прямой `fetch` живёт здесь, а не в компонентах и не в методах верхних слоёв.
```ts
// src/infrastructure/pet-project-api/client.ts
import { PetProjectApiError } from './errors/pet-project-api.error'
import type { QueryParams } from './types/client'
export class PetProjectApiClient {
constructor(
private readonly baseUrl: string,
private readonly defaultHeaders: Record<string, string> = {},
) {}
async get<T>(path: string, params: QueryParams = {}): Promise<T> {
const base = `${this.baseUrl.replace(/\/+$/, '')}/`
const url = new URL(path.replace(/^\/+/, ''), base)
Object.entries(params).forEach(([key, value]) => {
url.searchParams.set(key, String(value))
})
const response = await fetch(url, {
headers: {
Accept: 'application/json',
...this.defaultHeaders,
},
})
if (!response.ok) {
throw new PetProjectApiError(response.status, response.statusText)
}
return response.json() as Promise<T>
}
}
```
Это минимальный шаблон. Авторизация, дополнительные заголовки, `next.revalidate`, `post`, `formdata` и другие детали добавляются только когда они реально нужны API.
## Методы API
Методы группируются по сущностям в `methods/`. Они не знают про React, SWR и UI.
```ts
// src/infrastructure/pet-project-api/methods/posts.ts
import type { PetProjectApiClient } from '../client'
import type { PostDto, PostListQueryDto } from '../types/post'
export function postsMethods(client: PetProjectApiClient) {
return {
/** GET /posts */
list: (query: PostListQueryDto = {}) =>
client.get<PostDto[]>('posts', query),
/** GET /posts/{slug} */
get: (slug: string) =>
client.get<PostDto>(`posts/${slug}`),
}
}
```
Метод возвращает DTO в форме API. Если данным нужен доменный смысл, маппинг делается выше, в `business/`.
## Публичный API
`index.ts` собирает именованный API-объект и открывает наружу только публичные части модуля.
```ts
// src/infrastructure/pet-project-api/index.ts
import { PetProjectApiClient } from './client'
import { postsMethods } from './methods/posts'
const client = new PetProjectApiClient(
process.env.NEXT_PUBLIC_API_URL ?? '',
{ 'Content-Type': 'application/json' },
)
export const petProjectApi = {
posts: postsMethods(client),
}
export { PetProjectApiError } from './errors/pet-project-api.error'
export type { PostDto, PostListQueryDto } from './types'
export * from './hooks'
```
Внешний код импортирует только из `infrastructure/pet-project-api`, не из внутренних файлов модуля.
## Правила
- `fetch` используется только внутри базового клиента.
- DTO запросов и ответов живут в `types/`.
- `client.ts` не содержит DTO, GET-хуки и бизнес-логику.
- Методы лежат в `methods/` и возвращают DTO.
- GET-хуки добавляются отдельно в `hooks/`, если данные нужны в Client Components.
- Доменные типы и маппинг DTO живут не в REST-клиенте, а в `business/`.
Следующий шаг: [GET-хуки REST-клиента](./hooks.md) или [Стратегии получения данных](../strategies/index.md).