11 KiB
title, description, keywords
| title | description | keywords | |||||||
|---|---|---|---|---|---|---|---|---|---|
| Ручное создание REST-клиента | Создание generated-style REST-клиента вручную, когда OpenAPI нет или он неполный. |
|
Ручное создание REST-клиента
Ручной клиент используется, когда у API нет OpenAPI-спецификации или она недостаточно точная для автогенерации.
Ручной режим не является отдельной архитектурой. Это тот же generated-style клиент, где operation-функции написаны вручную вместо генерации из OpenAPI.
Зависимость
Для ручного клиента установите @gromlab/api-codegen@5.1.0+ как runtime-зависимость проекта.
bun add @gromlab/api-codegen
Не используйте npx @gromlab/api-codegen для ручного режима: npx нужен для генерации SDK из OpenAPI, а ручной клиент импортирует runtime API пакета напрямую.
Что нужно создать
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-файлы не объявляют доменные типы.
// 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
}
// 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 и бизнес-логику.
// 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,
})
// 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,
})
// 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,
})
// 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.
// 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-клиента.
Operations Tree
operations-tree.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.
// 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-клиентом:
await petProjectRestApi.posts.create({ title: 'Новый пост' })
await petProjectRestApi.posts.detail({ slug: 'hello' })
GET-Хуки
GET-хуки ручного клиента пишутся так же, как hooks для generated operations: импортируют точечную operation и вызывают её через общий *HttpClient.
// 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)
}
// src/infra/pet-project-rest-api/hooks/index.ts
'use client'
export { getPostListKey, useGetPostList } from './use-get-post-list.hook'
Публичный API
// 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, customRestApiClientclass,methods/*Methods(client)и ручныеget/postwrappers. - DTO запросов и ответов живут в
types/. - Доменные типы и маппинг DTO живут не в REST-клиенте, а в
business/.
Следующий шаг: Использование REST-клиента, GET-хуки REST-клиента или выбор route-level data fetching по nextjs-style-guide.