Construindo uma API com Elysia.js e Bun
No mundo do backend com JavaScript, existe uma busca constante: como aproveitar a produtividade e a flexibilidade da linguagem sem abrir mão de performance e segurança de tipos?
A resposta pode estar em um framework relativamente novo, mas cheio de potencial: Elysia.js.
Rodando sobre o Bun — um runtime super rápido — o Elysia não é “só mais um framework”. Ele promete unir o melhor dos dois mundos:
Performance superior a frameworks consagrados como Fastify e Gin.
Type-safety total, do request à resposta.
Ecossistema de plugins pronto para usar.
Parece bom demais pra ser verdade? Vamos colocar à prova construindo juntos uma API prática com Elysia.
Começando
Antes de tudo, precisamos instalar o Bun. Se ainda não tiver, pode instalar via npm:
npm install -g bun
Com o Bun instalado, vamos criar nossa aplicação já com o template oficial do Elysia:
bun create elysia app
cd app
Agora basta rodar:
bun dev
E acessar http://localhost:3000. Se tudo deu certo, você verá algo como Hello Elysia.
Criando nossos primeiros endpoints
Vamos começar simples, para entendermos a estrutura básica. Por padrão, você já tem uma rota /
. Vamos adicionar outra para listar nossos "To-Dos":
import { Elysia } from "elysia";
const app = new Elysia()
.get("/", () => "Hello Elysia")
.get("/todo", () => "my todos") // Nosso novo endpoint
.listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
Fácil, né? Mas a gente sabe que uma API de verdade precisa validar os dados que entram. É aqui que o Elysia brilha.
Por padrão, ele usa uma lib chamada TypeBox, mas o ecossistema é flexível. Como muitos de nós já usamos e amamos o Zod, vamos integrá-lo ao projeto.
Primeiro, instale o Zod:
bun add zod
Agora, vamos criar uma rota que recebe um id
como parâmetro e usar o Zod para garantir que ele seja do tipo correto.
import { Elysia } from "elysia";
import { z } from "zod"; // Importamos o Zod
const app = new Elysia()
.get("/", () => "Hello Elysia")
.get("/todo", () => "my todos")
.get(
"/todo/:id",
({ params }) => {
// O `params.id` aqui já é totalmente tipado!
return {
id: params.id,
};
},
{
// Aqui definimos o schema de validação para os parâmetros da URL
params: z.object({
id: z.string(),
}),
}
)
.listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
Com o mesmo princípio, vamos criar um endpoint POST para criar um novo "To-Do", validando o corpo (body) da requisição:
.post("/todo", ({ body }) => {
// `body` também é 100% tipado, graças ao Zod.
return {
title: body.title,
description: body.description,
}
},{
// Validando o corpo da requisição
body: z.object({
title: z.string(),
description: z.string(),
}),
})
Viu como é limpo e declarativo? A validação fica junto da rota, tornando o código fácil de ler e manter.
Documentação Automática? Temos!
Um erro clássico de muitos desenvolvedores é deixar a documentação para depois (ou seja, pra nunca). Com o Elysia, gerar uma documentação profissional no padrão OpenAPI (Swagger) é ridiculamente fácil.
Primeiro, instale o plugin:
@elysiajs/openapi
Agora vamos adicionar no nosso codigo. Basta adicionar o seguinte trecho
.use(openapi({
mapJsonSchema: {
zod: z.toJSONSchema,
},
}))
Agora, basta usar o plugin na nossa instância do Elysia. Como estamos usando Zod, precisamos de uma pequena configuração para que ele saiba como "traduzir" nossos schemas.
.use(openapi({
mapJsonSchema: {
zod: z.toJSONSchema,
},
}))
Dica: Se você estivesse usando o TypeBox (padrão do Elysia), não precisaria do mapJsonSchema. Ele funcionaria direto da caixa.
Veja como ficou nosso código completo até agora:
import { openapi } from "@elysiajs/openapi";
import { Elysia } from "elysia";
import { z } from "zod";
const app = new Elysia()
.use(openapi({
mapJsonSchema: {
zod: z.toJSONSchema,
},
}))
.get("/", () => "Hello Elysia")
.get("/todo", () => "my todos")
.get(
"/todo/:id",
({ params }) => {
return {
id: params.id,
};
},
{
params: z.object({
id: z.string(),
}),
}
)
.post(
"/todo",
({ body }) => {
return {
title: body.title,
description: body.description,
};
},
{
body: z.object({
title: z.string(),
description: z.string(),
}),
}
)
.listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
Rode o projeto novamente e acesse http://localhost:3000/openapi. Você verá uma interface do Scalar com todos os seus endpoints já documentados. Mágico!
Deixando a API Funcional
Até agora, nossas rotas não fazem muita coisa. Vamos adicionar um pouco de lógica para simular um CRUD de "To-Dos", aproveitando para enriquecer ainda mais nossa documentação.
import { openapi } from "@elysiajs/openapi";
import { randomUUIDv7 } from "bun";
import { Elysia, NotFoundError } from "elysia";
import { z } from "zod";
// Tipagem para nosso "To-Do"
type Todo = {
id: string;
title: string;
description: string;
};
// Simulando um "banco de dados" em memória
const todos: Todo[] = [];
const app = new Elysia()
.use(
openapi({
mapJsonSchema: {
zod: z.toJSONSchema,
},
})
)
.get(
"/todo",
() => {
return todos;
},
{
// Adicionando detalhes para a documentação
detail: {
summary: "Get all todos",
},
// Tipando a resposta da requisição
response: {
200: z.array(
z.object({
id: z.string(),
title: z.string(),
description: z.string(),
})
),
},
}
)
.get(
"/todo/:id",
({ params }) => {
const todo = todos.find((todo) => todo.id === params.id);
if (!todo) {
// Lançando um erro padrão do Elysia
throw new NotFoundError("Todo not found");
}
return todo;
},
{
params: z.object({
id: z.string(),
}),
detail: {
summary: "Get todo by id",
},
response: {
// Agora nossa documentação sabe o que esperar em cada cenário
200: z.object({
id: z.string(),
title: z.string(),
description: z.string(),
}),
404: z.string(),
},
}
)
.post(
"/todo",
({ body, set }) => {
const todo = {
id: randomUUIDv7(),
title: body.title,
description: body.description,
};
todos.push(todo);
// Definindo o status code da resposta
set.status = 201;
return todo;
},
{
body: z.object({
title: z.string(),
description: z.string(),
}),
detail: {
summary: "Create todo",
},
response: {
201: z.object({
id: z.string(),
title: z.string(),
description: z.string(),
}),
},
}
)
.listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
O que nós fizemos aqui?
"Banco de Dados" Fake: Criamos um array todos em memória para armazenar nossos dados e um type Todo para manter tudo organizado.
Documentação turbinada: Usamos os campos detail e response para descrever o que cada endpoint faz e, mais importante, para tipar exatamente o que eles retornam em cada status code (200, 201, 404). Isso não só deixa o Swagger lindo, mas também garante que o TypeScript vai te avisar se você tentar retornar algo diferente do prometido.
Lógica de Negócio: Implementamos a busca e a criação de "To-Dos", usando o NotFoundError nativo do Elysia para lidar com casos onde o item não é encontrado.
Tratamento de Erros Centralizado
No endpoint /todo/:id, quando um "To-Do" não é encontrado, lançamos um NotFoundError. O retorno padrão pode não ser o ideal para sua API. Que tal customizarmos isso?
O Elysia permite criar um "handler" de erros global. Adicione este trecho ao final da sua cadeia de métodos:
.onError(({ error }) => {
if (error instanceof NotFoundError) {
return {
status: 404,
body: { message: error.message },
};
}
})
Dessa forma, sempre que um NotFoundError
for lançado em qualquer lugar da sua aplicação, ele será capturado e formatado da maneira que você definiu. Isso é ótimo para padronizar as respostas de erro da sua API.
Bônus: Gerando um Executável
Uma das funcionalidades mais incríveis do Bun é a capacidade de compilar seu projeto TypeScript em um único executável. Chega de node_modules em produção!
Para fazer isso, rode o seguinte comando no seu terminal:
bun build ./src/index.ts --target=bun --minify --compile --outfile app
Isso vai gerar um arquivo app
(ou app.exe
no Windows). Agora, para rodar sua API, basta executar esse arquivo:
./app
E pronto! Sua aplicação, completa e otimizada, rodando a partir de um único binário.
Conclusão
Neste artigo, demos só uma palhinha do que o Elysia e o Bun podem fazer. Vimos como é simples criar uma API robusta, com validação, documentação automática e performance de alto nível, tudo isso com uma sintaxe elegante e intuitiva.
O ecossistema ainda tem muito a oferecer, como middlewares, autenticação com JWT, WebSockets e muito mais.
Espero que este guia tenha te animado a explorar essas ferramentas. Se curtiu, deixa um feedback! Quem sabe a gente não transforma isso em uma série, construindo uma aplicação completa e explorando as melhores práticas de ponta a ponta. Valeu!
Top comments (0)