188 lines
6.5 KiB
Markdown
188 lines
6.5 KiB
Markdown
|
|
---
|
|||
|
|
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).
|