DEV Community

Matheus Lucas
Matheus Lucas

Posted on

Elysia: O melhor web framework

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 
Enter fullscreen mode Exit fullscreen mode

Com o Bun instalado, vamos criar nossa aplicação já com o template oficial do Elysia:

bun create elysia app
cd app
Enter fullscreen mode Exit fullscreen mode

Agora basta rodar:

bun dev
Enter fullscreen mode Exit fullscreen mode

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}`
);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Agora, vamos criar uma rota que recebe um idcomo 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}`
);
Enter fullscreen mode Exit fullscreen mode

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(),
    }),
  })
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Agora vamos adicionar no nosso codigo. Basta adicionar o seguinte trecho

 .use(openapi({
      mapJsonSchema: {
        zod: z.toJSONSchema,
      },
  }))
Enter fullscreen mode Exit fullscreen mode

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,
      },
  }))
Enter fullscreen mode Exit fullscreen mode

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}`
);
Enter fullscreen mode Exit fullscreen mode

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}`
);
Enter fullscreen mode Exit fullscreen mode

O que nós fizemos aqui?

  1. "Banco de Dados" Fake: Criamos um array todos em memória para armazenar nossos dados e um type Todo para manter tudo organizado.

  2. 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.

  3. 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 },
      };
    }
  })
Enter fullscreen mode Exit fullscreen mode

Dessa forma, sempre que um NotFoundErrorfor 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 
Enter fullscreen mode Exit fullscreen mode

Isso vai gerar um arquivo app(ou app.exe no Windows). Agora, para rodar sua API, basta executar esse arquivo:

./app
Enter fullscreen mode Exit fullscreen mode

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)