# Типизированные клиенты

> Перевод страницы с английского оригинала. При расхождениях приоритет имеет [английская версия](/docs/typed-client).

DaloyJS предлагает **два** способа вызывать ваш API с полной типобезопасностью. Используйте тот, что подходит вашему потребителю.

## 1. Типизированный клиент внутри процесса (без кодогенерации)

Для потребителей на TypeScript в том же монорепозитории (тесты, внутренние инструменты, серверные действия Next.js):

```ts
import { createInProcessClient } from "@daloyjs/core/client";
import { app } from "./app.js";

const client = createInProcessClient(app);

const r = await client.getBookById({ params: { id: "1" } });
//    ^? { status: 200; body: { id: string; title: string } }
//      | { status: 404; body: ProblemJson }

if (r.status === 200) {
  console.log(r.body.title); // string, полностью типизировано
}
```

Ключами клиента служат значения `operationId`, он возвращает размеченное объединение `{status, body, headers}` и выводит всё из самого определения маршрута. Шаг сборки не нужен.

### Параметры пути

Параметры пути подставляются целыми сегментами, поэтому `:id` никогда не совпадёт внутри `:idx`, а каждое значение percent-кодируется в собственный сегмент. Начиная с 1.3.7 сгенерированный метод выбрасывает `TypeError` ещё до отправки запроса, если параметр отсутствует, пуст, равен `.` или `..`. Разбор URL разрешил бы точечный сегмент (даже в виде `%2E%2E`) и молча перенаправил бы вызов, например превратив `DELETE /orgs/:org/members/..` в `DELETE /orgs/:org`. Wildcard-маршруты (`*path` или голый `*` с именем `wildcard`) принимают значение, разделённое `/`; каждый сегмент кодируется и проверяется так же.

```ts
// app.get("/files/*path", { operationId: "getFile", ... })
await client.getFile({ params: { path: "docs/guide.md" } }); // GET /files/docs/guide.md

await client.getBookById({ params: { id: ".." } });
// TypeError: Invalid path parameter "id" for /books/:id: empty and "."/".." segments are not allowed
```

> [!TIP]
> **Собирайте кортежи маршрутов, а не расширяйте тип App.**
> Экспортируйте файлы маршрутов через `defineRoute()` и регистрируйте литеральный кортеж через `app.registerRoutes([...])`. Цепочка вызовов `route()` тоже работает. Две вещи намеренно стирают вывод типов и сводят клиент к нестрогой поверхности:
>
> - Аннотация экземпляра голым `const app: App = ...` (или возврат `: App` из фабрики). Расширяющая аннотация отбрасывает накопленные маршруты, поэтому позвольте типу вывестись самостоятельно.
> - Регистрация маршрутов отдельными инструкциями на ранее объявленной переменной вместо возврата результата цепочки или использования `registerRoutes()`.
>
> Руководство по модульному монолиту показывает композицию в нескольких файлах с кортежами маршрутов из нескольких ограниченных контекстов. `group()` с колбэком и `register()` плагинов по-прежнему дают изоляцию во время выполнения, но их колбэки не могут расширить TypeScript-дженерик родительской переменной. Используйте кортежи маршрутов как границу композиции для типизированного клиента.

## 2. SDK Hey API (для других языков и репозиториев, во время сборки)

Для потребителей вне монорепозитория или на других языках сгенерируйте полностью типизированный fetch SDK с помощью [@hey-api/openapi-ts](https://heyapi.dev/openapi-ts/get-started).

**Diagram: Конвейер кодогенерации**

1. **Маршруты** - app.route(...)
2. **generateOpenAPI** - @daloyjs/core/openapi
3. **openapi.json** - спецификация OpenAPI 3.1 на диске
4. **openapi-ts** - генератор Hey API
5. **Типизированный SDK** - sdk.gen.ts · types.gen.ts
6. **Потребитель** - полностью типизированные вызовы fetch

pnpm gen запускает всю цепочку: выгружает спецификацию из ваших маршрутов, затем Hey API превращает её в типизированный fetch SDK. Запускайте его заново при каждом изменении маршрута, и клиент будет идти в ногу с контрактом.

```bash
pnpm add -D @hey-api/openapi-ts prettier
```

```ts
// openapi-ts.config.ts
import { defineConfig } from "@hey-api/openapi-ts";

export default defineConfig({
  input: "./generated/openapi.json",
  output: { path: "./generated/client", postProcess: ["prettier"] },
  plugins: ["@hey-api/client-fetch", "@hey-api/typescript", "@hey-api/sdk"],
});
```

```json
// package.json
"scripts": {
  "gen:openapi": "node scripts/dump-openapi.ts",
  "gen:client":  "openapi-ts",
  "gen":         "pnpm gen:openapi && pnpm gen:client"
}
```

```bash
pnpm gen
# записывает:
#   generated/openapi.json
#   generated/client/{client.gen.ts, sdk.gen.ts, types.gen.ts, index.ts}
```

## Использование сгенерированного SDK

```ts
import { client } from "./generated/client/client.gen.js";
import { getBookById } from "./generated/client/sdk.gen.js";

client.setConfig({ baseUrl: "https://api.example.com" });

const { data, error } = await getBookById({ path: { id: "1" } });
if (error) console.error(error);
else if (data) console.log(data.title);
```

## Что выбрать?

| Сценарий | Выбор |
| --- | --- |
| Вызывающий код на TypeScript в том же репозитории (тесты, внутренние инструменты) | `createInProcessClient` |
| Веб-приложение или мобильный бандл RN в отдельном репозитории | SDK Hey API |
| Потребитель не на TypeScript (Python, Swift, Kotlin) | Документ OpenAPI + предпочитаемый им генератор |
| Публичный SDK для третьих сторон | SDK Hey API, опубликованный отдельным пакетом |

## Переходите с ts-rest?

[ts-rest](https://ts-rest.com/) является популярной библиотекой в стиле contract-first, которая даёт сквозные типы TypeScript **без кодогенерации**, разделяя контракт (`initContract`) между сервером на адаптерах (Express, Fastify, NestJS, Next.js) и fetch-клиентом (`initClient`). Если вам нравится такая модель, DaloyJS покажется знакомым, но с двумя отличиями.

Во-первых, в DaloyJS **контрактом является само определение маршрута**, и отдельного объекта контракта, который нужно синхронизировать, нет. Показанный выше `createClient` внутри процесса даёт тот же опыт без кодогенерации и с общими типами для вызывающего кода на TypeScript в том же репозитории.

Во-вторых, типобезопасность ts-rest работает только в TypeScript и требует, чтобы клиент импортировал контракт. DaloyJS из тех же маршрутов генерирует полноценную спецификацию **OpenAPI 3.1** и SDK Hey API, поэтому покрыты и потребители, которые не могут импортировать ваши типы (другие репозитории, другие языки, публичные SDK). В ts-rest OpenAPI является необязательным дополнением (`@ts-rest/open-api`). Кроме того, DaloyJS сам является сервером и рантаймом, переносимым между Node, Bun, Deno, Cloudflare и Vercel, а не слоем типизации поверх отдельного фреймворка.

|  | ts-rest | DaloyJS |
| --- | --- | --- |
| Источник контракта | Отдельный объект `initContract` | Само определение маршрута |
| Типизированный клиент без кодогенерации | Да (`initClient`, только TypeScript) | Да (`createClient`, только TypeScript) |
| Клиенты для других языков и репозиториев | Дополнение OpenAPI (`@ts-rest/open-api`) | OpenAPI 3.1 + SDK Hey API, встроено |
| Сервер | Адаптер для Express / Fastify / NestJS / Next.js | Встроенный, переносимый между рантаймами |
| Валидация во время выполнения | Standard Schema (Zod / Valibot / ArkType) | Standard Schema (Zod / Valibot / ArkType / TypeBox) |
| Безопасность по умолчанию | На ваше усмотрение | Заголовки и лимиты тела включены по умолчанию; CSRF и ограничение частоты в одной строке |

Нужен контракт побольше, чтобы проверить вывод генератора? Используйте [большое демо фейкового REST API](/docs/tutorials/fake-rest-api) (на английском) как стресс-тест вместо минимального учебного приложения.

Последнее обновление: 30 октября 2026 г.

---

Source: https://daloyjs.dev/ru/docs/typed-client