# Clientes tipados

> Traducción de la página original en inglés. Si algo no cuadra, prevalece la [versión en inglés](/docs/typed-client).

DaloyJS ofrece **dos** formas de llamar a tu API con seguridad de tipos completa. Usa la que encaje con tu consumidor.

## 1. Cliente tipado en proceso (sin generación de código)

Para consumidores en TypeScript dentro del mismo monorepo (pruebas, herramientas internas, server actions de 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, totalmente tipado
}
```

El cliente se indexa por `operationId`, devuelve una unión discriminada de `{status, body, headers}` e infiere todo a partir de la propia definición de la ruta. Sin paso de build.

### Parámetros de ruta

Los parámetros de ruta se sustituyen por segmentos completos, así que `:id` nunca coincide dentro de `:idx`, y cada valor se codifica con porcentajes en su propio segmento. Desde la 1.3.7, un método generado lanza un `TypeError`, antes de enviar ninguna petición, cuando un parámetro falta, está vacío o es `.` o `..`. El parseo de URL resolvería un segmento de punto (incluso como `%2E%2E`) y cambiaría en silencio el destino de la llamada, por ejemplo convirtiendo `DELETE /orgs/:org/members/..` en `DELETE /orgs/:org`. Las rutas comodín (`*path`, o un `*` a secas con el nombre `wildcard`) aceptan un valor separado por `/`; cada segmento se codifica y se comprueba de la misma forma.

```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]
> **Compón tuplas de rutas en lugar de ensanchar la App.**
> Exporta los archivos de rutas con `defineRoute()` y registra una tupla literal con `app.registerRoutes([...])`. Las llamadas encadenadas a `route()` también funcionan. Hay dos cosas que borran la inferencia a propósito y reducen el cliente a una superficie laxa:
>
> - Anotar la instancia con un `const app: App = ...` a secas (o devolver `: App` desde una fábrica). La anotación que ensancha el tipo descarta las rutas acumuladas, así que deja que el tipo se infiera.
> - Registrar las rutas como sentencias separadas sobre una variable declarada previamente, en lugar de devolver el resultado encadenado o usar `registerRoutes()`.
>
> La guía del monolito modular muestra una composición en varios archivos con tuplas de rutas de varios contextos acotados. `group()` con callback y `register()` de los plugins siguen ofreciendo encapsulación en tiempo de ejecución, pero sus callbacks no pueden ensanchar el genérico de TypeScript de la variable padre. Usa tuplas de rutas como límite de composición del cliente tipado.

## 2. SDK de Hey API (entre lenguajes, entre repositorios, en tiempo de build)

Para los consumidores que están fuera del monorepo o en otros lenguajes, genera un SDK fetch totalmente tipado con [@hey-api/openapi-ts](https://heyapi.dev/openapi-ts/get-started).

**Diagram: Cadena de generación de código**

1. **Rutas** - app.route(...)
2. **generateOpenAPI** - @daloyjs/core/openapi
3. **openapi.json** - especificación OpenAPI 3.1 en disco
4. **openapi-ts** - generador de Hey API
5. **SDK tipado** - sdk.gen.ts · types.gen.ts
6. **Consumidor** - llamadas fetch totalmente tipadas

pnpm gen ejecuta toda la cadena: vuelca la especificación desde tus rutas y deja que Hey API la convierta en un SDK fetch tipado. Vuelve a ejecutarlo cada vez que cambie una ruta y el cliente irá siempre al compás del contrato.

```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
# escribe:
#   generated/openapi.json
#   generated/client/{client.gen.ts, sdk.gen.ts, types.gen.ts, index.ts}
```

## Usar el SDK generado

```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);
```

## ¿Cuál debo usar?

| Caso de uso | Elección |
| --- | --- |
| Código TypeScript en el mismo repositorio (pruebas, herramientas internas) | `createInProcessClient` |
| Aplicación web o bundle móvil de React Native en otro repositorio | SDK de Hey API |
| Consumidor que no usa TypeScript (Python, Swift, Kotlin) | Documento OpenAPI + su generador preferido |
| SDK público para terceros | SDK de Hey API, publicado como paquete propio |

## ¿Vienes de ts-rest?

[ts-rest](https://ts-rest.com/) es una librería popular basada en contratos que te da tipos TypeScript de extremo a extremo **sin generación de código** compartiendo un contrato (`initContract`) entre un servidor basado en adaptadores (Express, Fastify, NestJS, Next.js) y un cliente fetch (`initClient`). Si te gusta ese modelo, DaloyJS te resultará familiar, con dos diferencias.

La primera: en DaloyJS **la definición de la ruta es el contrato**; no hay un objeto de contrato aparte que mantener sincronizado. El `createClient` en proceso que has visto arriba ofrece la misma experiencia sin generación de código y con tipos compartidos para el código TypeScript del mismo repositorio.

La segunda: la seguridad de tipos de ts-rest es solo para TypeScript y obliga al cliente a importar el contrato. DaloyJS emite una especificación **OpenAPI 3.1** de primera clase y un SDK de Hey API a partir de las mismas rutas, así que también cubre a los consumidores que no pueden importar tus tipos (otros repositorios, otros lenguajes, SDK públicos). En ts-rest, OpenAPI es un complemento opcional (`@ts-rest/open-api`). Además, DaloyJS es el propio servidor y runtime, portable entre Node, Bun, Deno, Cloudflare y Vercel, y no una capa de tipos montada sobre otro framework.

|  | ts-rest | DaloyJS |
| --- | --- | --- |
| Origen del contrato | Objeto `initContract` aparte | La propia definición de la ruta |
| Cliente tipado sin generación de código | Sí (`initClient`, solo TypeScript) | Sí (`createClient`, solo TypeScript) |
| Clientes entre lenguajes / entre repositorios | Complemento de OpenAPI (`@ts-rest/open-api`) | OpenAPI 3.1 + SDK de Hey API, de primera clase |
| Servidor | Adaptador sobre Express / Fastify / NestJS / Next.js | Integrado y portable entre runtimes |
| Validación en tiempo de ejecución | Standard Schema (Zod / Valibot / ArkType) | Standard Schema (Zod / Valibot / ArkType / TypeBox) |
| Seguridad por defecto | Te la montas tú | Cabeceras y límites de cuerpo activados por defecto; CSRF y limitación de peticiones a una línea |

¿Necesitas un contrato más grande para validar lo que genera tu generador? Usa la [demo REST falsa de gran tamaño](/docs/tutorials/fake-rest-api) (en inglés) como caso de estrés en lugar de una aplicación mínima de tutorial.

Última actualización: 30 de octubre de 2026

---

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