Skip to content

Eden Treaty é a biblioteca cliente oficial do Elysia que gera um cliente API com tipagem segura diretamente do tipo da sua aplicação Elysia. Como nestelia usa o Elysia como camada HTTP, o Eden Treaty funciona imediatamente.

Instalação

bash
bun add @elysiajs/eden

Por que rotas com decoradores não produzem tipos automaticamente

nestelia registra rotas em tempo de execução via Reflect.getMetadata. O TypeScript vê os argumentos dos decoradores (por exemplo, o caminho em @Get('/users')) como string — não como o tipo literal '/users'. Isso significa que o parâmetro genérico da instância Elysia nunca aprende sobre as rotas apenas pelo caminho do decorador.

A solução é um schema tipado que descreve a superfície da sua API — gerado automaticamente pelo CLI nestelia-gen ou escrito manualmente (junto ao controlador).

Geração automática — nestelia-gen

nestelia-gen analisa estaticamente seus controladores usando o compilador TypeScript e gera um app.schema.ts completamente tipado — incluindo os tipos de resposta das anotações dos seus métodos. Sem inicialização da aplicação, sem efeitos colaterais em tempo de execução.

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

Adicione anotações de tipo de retorno aos métodos do seu controlador para que o gerador possa identificá-los:

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 { … }
}

O app.schema.ts gerado tem esta aparência:

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;

As expressões TypeBox de body/params são copiadas literalmente do seu arquivo de controlador. Os tipos de resposta vêm das anotações dos seus métodos. Execute nestelia-gen novamente sempre que adicionar ou alterar rotas.

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

Geração automática na inicialização — opção gen

Em vez de executar nestelia-gen como um script separado, passe gen: true para createElysiaApplication e o schema será regenerado automaticamente toda vez que a aplicação iniciar:

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

Para personalizar o caminho de saída ou o tsconfig, passe um objeto:

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

Isso é equivalente a executar bunx nestelia-gen [args] antes do bootstrap — útil em desenvolvimento para não precisar lembrar de re-executar o CLI manualmente.

Em seguida, use o schema com withSchema() e 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 ao controlador

Para equipes que preferem schemas explícitos sem uma etapa de compilação, exporte um schema Elysia escrito à mão ao lado de cada controlador. O schema é a única fonte de verdade para os tipos do cliente; o controlador é a única fonte de verdade para a lógica de negócio.

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

Componha os schemas de todos os módulos em 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);

Como withSchema() funciona

app.withSchema(schema) faz um único cast em tempo de execução — this.httpServer as unknown as TSchema — e o retorna. O servidor Nestelia ativo (com todas as rotas dos controladores compiladas) é retornado em tempo de execução; o TypeScript o vê como o tipo do schema com todos os genéricos de rota. Zero overhead, zero duplicação do tratamento de requisições.

Testes sem listen()

Passe o servidor tipado diretamente para treaty — o método handle() do Elysia processa as requisições no mesmo processo:

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)); // sem necessidade de listen()

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

Lançado sob a licença MIT.