Skip to content

Eden Treaty es la biblioteca cliente oficial de Elysia que genera un cliente API con tipado seguro directamente desde el tipo de tu aplicación Elysia. Como nestelia usa Elysia como capa HTTP, Eden Treaty funciona sin configuración adicional.

Instalación

bash
bun add @elysiajs/eden

Por qué las rutas con decoradores no producen tipos automáticamente

nestelia registra rutas en tiempo de ejecución mediante Reflect.getMetadata. TypeScript ve los argumentos de los decoradores (por ejemplo, la ruta en @Get('/users')) como string, no como el tipo literal '/users'. Esto significa que el parámetro genérico de la instancia de Elysia nunca aprende sobre las rutas solo a través del decorador.

La solución es un schema tipado que describe la superficie de tu API — generado automáticamente por el CLI nestelia-gen o escrito manualmente (junto al controlador).

Generación automática — nestelia-gen

nestelia-gen analiza estáticamente tus controladores usando el compilador de TypeScript y genera un app.schema.ts completamente tipado — incluyendo los tipos de respuesta de las anotaciones de tus métodos. Sin arranque de la aplicación, sin efectos secundarios en tiempo de ejecución.

bash
bunx nestelia-gen --tsconfig tsconfig.json src/app.schema.ts

Añade anotaciones de tipo de retorno a los métodos de tu controlador para que el generador pueda detectarlos:

typescript
@Controller("/users")
export class UsersController {
  @Get("/")
  getAll(): User[] { … }

  @Get("/:id")
  getOne(@Param(IdParams) p: Static<typeof IdParams>): User | null { … }

  @Post("/")
  create(@Body(CreateDto) body: Static<typeof CreateDto>): User { … }
}

El app.schema.ts generado tiene este aspecto:

typescript
// auto-generated by nestelia-gen — do not edit manually
import { Elysia, t } from "elysia";
import type { User } from "./users/user.entity";

export const appSchema = new Elysia()
  .get("/users", (): User[] => undefined as never)
  .get("/users/:id", (): User | null => undefined as never, {
    params: t.Object({ id: t.String() })
  })
  .post("/users", (): User => undefined as never, {
    body: t.Object({ name: t.String() })
  });

export type App = typeof appSchema;

Las expresiones TypeBox de body/params se copian literalmente de tu archivo de controlador. Los tipos de respuesta provienen de las anotaciones de tus métodos. Vuelve a ejecutar nestelia-gen cada vez que añadas o cambies rutas.

json
// package.json
{
  "scripts": {
    "gen": "nestelia-gen --tsconfig tsconfig.json src/app.schema.ts",
    "build": "bun run gen && tsc"
  }
}

Generación automática al iniciar — opción gen

En lugar de ejecutar nestelia-gen como un script separado, pasa gen: true a createElysiaApplication y el schema se regenerará automáticamente cada vez que la aplicación arranque:

typescript
const app = await createElysiaApplication(AppModule, { gen: true });

Para personalizar la ruta de salida o el tsconfig, pasa un objeto:

typescript
const app = await createElysiaApplication(AppModule, {
  gen: { output: "src/schema.ts", tsconfig: "tsconfig.app.json" },
});

Esto equivale a ejecutar bunx nestelia-gen [args] antes del bootstrap — útil en desarrollo para no tener que recordar volver a ejecutar el CLI manualmente.

Luego usa el schema con withSchema() y treaty:

typescript
// src/main.ts
import { createElysiaApplication } from "nestelia";
import { AppModule } from "./app.module";
import { appSchema } from "./app.schema"; // ← auto-generated

const app = await createElysiaApplication(AppModule);
const typedServer = app.withSchema(appSchema);
export type App = typeof typedServer;

await typedServer.listen(3000);
typescript
// src/client.ts
import { treaty } from "@elysiajs/eden";
import type { App } from "./main";

const client = treaty<App>("http://localhost:3000");

const { data } = await client.users.get();              // User[]
const { data: user } = await client.users.post({ name: "Alice" }); // User
const { data: found } = await client.users({ id: "1" }).get();     // User | null

Schema manual — junto al controlador

Para equipos que prefieren schemas explícitos sin un paso de compilación, exporta un schema Elysia escrito a mano junto a cada controlador. El schema es la única fuente de verdad para los tipos del cliente; el controlador es la única fuente de verdad para la lógica de negocio.

typescript
// src/users/users.controller.ts
import { Elysia, t, type Static } from "elysia";
import { Controller, Get, Post, Delete, Body, Param } from "nestelia";
import type { User } from "./user.entity";
import { UsersService } from "./users.service";

const IdParams = t.Object({ id: t.String() });
const CreateDto = t.Object({ name: t.String() });

export const usersSchema = new Elysia({ prefix: "/users" })
  .get("/", (): User[] => [])
  .post("/", (): User => ({} as User), { body: CreateDto })
  .get("/:id", (): User | null => null, { params: IdParams })
  .delete("/:id", (): { success: boolean } => ({ success: true }), { params: IdParams });

@Controller("/users")
export class UsersController {
  constructor(private readonly users: UsersService) {}

  @Get("/")    getAll(): User[] { return this.users.findAll(); }
  @Post("/")   create(@Body(CreateDto) body: Static<typeof CreateDto>): User { return this.users.create(body); }
  @Get("/:id") getOne(@Param(IdParams) p: Static<typeof IdParams>): User | null { return this.users.findOne(p.id); }
  @Delete("/:id") remove(@Param(IdParams) p: Static<typeof IdParams>): { success: boolean } { return this.users.remove(p.id); }
}

Compón los schemas de todos los módulos en main.ts:

typescript
// src/main.ts
import { createElysiaApplication } from "nestelia";
import { Elysia } from "elysia";
import { AppModule } from "./app.module";
import { usersSchema } from "./users/users.controller";
import { postsSchema } from "./posts/posts.controller";

const app = await createElysiaApplication(AppModule);
const typedServer = app.withSchema(new Elysia().use(usersSchema).use(postsSchema));
export type App = typeof typedServer;

await typedServer.listen(3000);

Cómo funciona withSchema()

app.withSchema(schema) realiza un único cast en tiempo de ejecución — this.httpServer as unknown as TSchema — y lo devuelve. El servidor Nestelia activo (con todas las rutas de los controladores compiladas) se devuelve en tiempo de ejecución; TypeScript lo ve como el tipo del schema con todos los genéricos de ruta. Sin sobrecarga, sin duplicación del manejo de solicitudes.

Pruebas sin listen()

Pasa el servidor tipado directamente a treaty — el método handle() de Elysia procesa las solicitudes en el mismo proceso:

typescript
import { treaty } from "@elysiajs/eden";
import { createElysiaApplication } from "nestelia";
import { AppModule } from "./app.module";
import { appSchema } from "./app.schema";

const app = await createElysiaApplication(AppModule);
const client = treaty<App>(app.withSchema(appSchema)); // no se necesita listen()

const { data } = await client.users.get(); // User[]

Publicado bajo la licencia MIT.