Files
slm-design/.opencode/skills/rest-client/reference/canons/manual.md
2026-08-01 09:31:08 +03:00

306 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: Ручное создание REST-клиента
description: Создание generated-style REST-клиента вручную, когда OpenAPI нет или он неполный.
keywords: [rest, ручной клиент, api-codegen, operation, ApiRequestClient, RequestParams, ContentType]
---
# Ручное создание REST-клиента
Ручной клиент используется, когда у API нет OpenAPI-спецификации или она недостаточно точная для автогенерации.
Ручной режим не является отдельной архитектурой. Это тот же generated-style клиент, где operation-функции написаны вручную вместо генерации из OpenAPI.
## Зависимость
Для ручного клиента установите `@gromlab/api-codegen@5.1.0+` как runtime-зависимость проекта.
```bash
bun add @gromlab/api-codegen
```
Не используйте `npx @gromlab/api-codegen` для ручного режима: `npx` нужен для генерации SDK из OpenAPI, а ручной клиент импортирует runtime API пакета напрямую.
## Что нужно создать
```text
src/infra/
└── pet-project-rest-api/
├── client.ts
├── rest-api.ts
├── operations-tree.ts
├── operations/
│ ├── posts-create.ts
│ ├── posts-detail.ts
│ ├── posts-list.ts
│ └── index.ts
├── hooks/
│ ├── lib/
│ │ └── create-query-string.ts
│ ├── use-get-post-list.hook.ts
│ └── index.ts
├── types/
│ ├── post.ts
│ └── index.ts
└── index.ts
```
| Файл | Роль |
|------|------|
| `client.ts` | Настройка и экспорт `*HttpClient` |
| `operations/` | Ручные operation-функции в стиле generated SDK |
| `operations-tree.ts` | Ручное дерево операций для `createApiClient` |
| `rest-api.ts` | Полный bound API через `createApiClient` |
| `types/` | DTO запросов, ответов и именованные response-типы |
| `hooks/` | GET-хуки REST-клиента, если данные нужны в Client Components |
| `index.ts` | Публичный API REST-модуля |
## Типы API
DTO запросов и ответов живут в `types/`. `client.ts` и operation-файлы не объявляют доменные типы.
```ts
// src/infra/pet-project-rest-api/types/post.ts
export type PostDto = {
id: string
slug: string
title: string
}
export type PostListQueryDto = {
limit?: number
category?: string
}
export type CreatePostPayload = {
title: string
}
```
```ts
// src/infra/pet-project-rest-api/types/index.ts
export type { CreatePostPayload, PostDto, PostListQueryDto } from './post'
```
Если данным нужен доменный смысл или маппинг DTO, делайте это выше, в `business/`, а не в REST-клиенте.
## Operation-Функции
Каждая ручная operation повторяет контракт generated operation:
- первым аргументом принимает `http: ApiRequestClient`;
- принимает typed params/body как последующие аргументы;
- последним аргументом принимает `requestParams: RequestParams = {}`;
- внутри вызывает только `http.request(...)`;
- не использует прямой `fetch`;
- не знает про React, SWR, UI и бизнес-логику.
```ts
// src/infra/pet-project-rest-api/operations/posts-list.ts
import type { ApiRequestClient, RequestParams } from '@gromlab/api-codegen'
import type { PostDto, PostListQueryDto } from '../types'
export const postsList = (
http: ApiRequestClient,
query: PostListQueryDto = {},
requestParams: RequestParams = {},
) =>
http.request<PostDto[]>({
path: '/posts',
method: 'GET',
query,
format: 'json',
...requestParams,
})
```
```ts
// src/infra/pet-project-rest-api/operations/posts-detail.ts
import type { ApiRequestClient, RequestParams } from '@gromlab/api-codegen'
import type { PostDto } from '../types'
export type PostsDetailParams = {
slug: string
}
export const postsDetail = (
http: ApiRequestClient,
{ slug }: PostsDetailParams,
requestParams: RequestParams = {},
) =>
http.request<PostDto>({
path: `/posts/${slug}`,
method: 'GET',
format: 'json',
...requestParams,
})
```
```ts
// src/infra/pet-project-rest-api/operations/posts-create.ts
import { ContentType } from '@gromlab/api-codegen'
import type { ApiRequestClient, RequestParams } from '@gromlab/api-codegen'
import type { CreatePostPayload, PostDto } from '../types'
export const postsCreate = (
http: ApiRequestClient,
body: CreatePostPayload,
requestParams: RequestParams = {},
) =>
http.request<PostDto>({
path: '/posts',
method: 'POST',
body,
type: ContentType.Json,
format: 'json',
secure: true,
...requestParams,
})
```
```ts
// src/infra/pet-project-rest-api/operations/index.ts
export { postsCreate } from './posts-create'
export { postsDetail } from './posts-detail'
export { postsList } from './posts-list'
export type { PostsDetailParams } from './posts-detail'
```
## Транспорт
`client.ts` настраивает и экспортирует только `*HttpClient`.
```ts
// src/infra/pet-project-rest-api/client.ts
import { HttpClient } from '@gromlab/api-codegen'
export const petProjectHttpClient = new HttpClient({
baseUrl: 'https://example.com/api',
})
```
Авторизация, refresh token, логирование и другие настройки транспорта задаются опциями и хуками `HttpClient` в этом же файле: [Кастомизация HTTP-клиента](./http-client.md).
## Operations Tree
`operations-tree.ts` вручную собирает дерево операций. Держите структуру такой, какой вы ожидаете видеть после будущей автогенерации.
```ts
// src/infra/pet-project-rest-api/operations-tree.ts
import { postsCreate } from './operations/posts-create'
import { postsDetail } from './operations/posts-detail'
import { postsList } from './operations/posts-list'
export const operationsTree = {
posts: {
create: postsCreate,
detail: postsDetail,
list: postsList,
},
} as const
```
## Полный API-Клиент
`rest-api.ts` собирает полный bound API через тот же `createApiClient`, что используется в generated SDK.
```ts
// src/infra/pet-project-rest-api/rest-api.ts
import { createApiClient } from '@gromlab/api-codegen'
import { petProjectHttpClient } from './client'
import { operationsTree } from './operations-tree'
export const petProjectRestApi = createApiClient(
petProjectHttpClient,
operationsTree,
)
```
После binding внешний вызов совпадает с generated-клиентом:
```ts
await petProjectRestApi.posts.create({ title: 'Новый пост' })
await petProjectRestApi.posts.detail({ slug: 'hello' })
```
## GET-Хуки
GET-хуки ручного клиента пишутся так же, как hooks для generated operations: импортируют точечную operation и вызывают её через общий `*HttpClient`.
```ts
// src/infra/pet-project-rest-api/hooks/use-get-post-list.hook.ts
import type { SWRConfiguration } from 'swr'
import useSWR from 'swr'
import { petProjectHttpClient } from '../client'
import { postsList } from '../operations/posts-list'
import type { PostDto, PostListQueryDto } from '../types'
export const getPostListKey = (params: PostListQueryDto = {}) => {
const searchParams = new URLSearchParams()
if (params.limit !== undefined) {
searchParams.set('limit', String(params.limit))
}
if (params.category) {
searchParams.set('category', params.category)
}
const search = searchParams.toString()
return ['pet-project-rest-api', `/posts${search ? `?${search}` : ''}`] as const
}
export const useGetPostList = (
params: PostListQueryDto = {},
config?: SWRConfiguration<PostDto[]>,
) => {
const key = getPostListKey(params)
const fetcher = () => postsList(petProjectHttpClient, params)
return useSWR<PostDto[]>(key, fetcher, config)
}
```
```ts
// src/infra/pet-project-rest-api/hooks/index.ts
'use client'
export { getPostListKey, useGetPostList } from './use-get-post-list.hook'
```
## Публичный API
```ts
// src/infra/pet-project-rest-api/index.ts
export { petProjectHttpClient } from './client'
export { petProjectRestApi } from './rest-api'
export * from './hooks'
export type { PostsDetailParams } from './operations'
export type { CreatePostPayload, PostDto, PostListQueryDto } from './types'
```
Внешний код импортирует только из `infra/pet-project-rest-api`, не из внутренних файлов модуля.
## Миграция На OpenAPI
Ручной клиент проектируйте так, чтобы его можно было заменить автогенерацией без смены API потребителей.
- Называйте operation-функции и дерево близко к будущим `operationId` и tags OpenAPI.
- Держите сигнатуры operation в стиле generated SDK: `operation(http, params/body, requestParams?)`.
- Не создавайте собственный класс клиента и методные фабрики.
- Если позже появится OpenAPI, сгенерируйте SDK и замените ручные operations на generated operations с минимальными правками `rest-api.ts`, GET-хуков и re-exports.
## Правила
- Ручной клиент использует `@gromlab/api-codegen@5.1.0+` как runtime dependency.
- `client.ts` экспортирует только `*HttpClient`.
- Ручные запросы живут в `operations/` и пишутся как generated-style operations.
- `operations-tree.ts` вручную собирает дерево для `createApiClient`.
- `rest-api.ts` экспортирует полный `*RestApi` через `createApiClient(*HttpClient, operationsTree)`.
- GET-хуки вызывают точечные operations через `*HttpClient`, не полный `*RestApi`.
- Не используйте прямой `fetch`, custom `RestApiClient` class, `methods/*Methods(client)` и ручные `get/post` wrappers.
- DTO запросов и ответов живут в `types/`.
- Доменные типы и маппинг DTO живут не в REST-клиенте, а в `business/`.
Следующий шаг: [Использование REST-клиента](../../SKILL.md#использование-rest-клиента), [GET-хуки REST-клиента](../../SKILL.md#get-хуки-rest-клиента) или выбор route-level data fetching по `nextjs-style-guide`.