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
bun add @elysiajs/edenPor 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.
bunx nestelia-gen --tsconfig tsconfig.json src/app.schema.tsAdicione anotações de tipo de retorno aos métodos do seu controlador para que o gerador possa identificá-los:
@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:
// 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.
// 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:
const app = await createElysiaApplication(AppModule, { gen: true });Para personalizar o caminho de saída ou o tsconfig, passe um objeto:
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:
// 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);// 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 | nullSchema 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.
// 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:
// 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:
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[]