--- 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 ``` ## Ошибка 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 = {}, ) {} async get(path: string, params: QueryParams = {}): Promise { 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 } } ``` Это минимальный шаблон. Авторизация, дополнительные заголовки, `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('posts', query), /** GET /posts/{slug} */ get: (slug: string) => client.get(`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).