Building Scalable APIs with Bun and Elysia: A Developer's Guide

May 2, 2025 at 12:10 PM
Bun
Elysia
TypeScript
API development
scalable systems
JavaScript
web development
backend
full-stack
programming

As developers, we’re always looking for tools that make our work faster, simpler, and more reliable. Recently, I’ve been exploring Bun and Elysia, a powerful combo for building scalable APIs. In this post, I’ll share why these tools are worth your attention, walk through a simple example, and explain how they can help you create high-performance APIs with ease.

Why Bun and Elysia?

Bun is a modern JavaScript runtime designed to be fast and lightweight. It’s a great alternative to Node.js, with built-in tools like a package manager and a test runner. Elysia, on the other hand, is a framework that runs on Bun (and Node.js) and makes building APIs straightforward with a clean syntax and excellent TypeScript support.

Together, they offer:

  • Speed: Bun’s optimized runtime makes APIs respond quickly.
  • Simplicity: Elysia’s intuitive syntax reduces boilerplate code.
  • Type Safety: Strong TypeScript integration catches errors early.
  • Scalability: Both tools handle high traffic with minimal overhead.

Getting Started: A Simple API

Let’s create a basic API for a to-do list app using Bun and Elysia. This example will show how to set up a server, handle GET and POST requests, and store data in memory.

Step 1: Set Up Your Project

First, make sure you have Bun installed. If not, run:

curl -fsSL https://bun.sh/install | bash

Create a new project folder and initialize it:

mkdir todo-api
cd todo-api
bun init

Install Elysia:

bun add elysia

Step 2: Build the API

Create a file called index.ts and add the following code:

import { Elysia } from 'elysia';

// In-memory storage for todos
interface Todo {
  id: number;
  text: string;
  completed: boolean;
}
let todos: Todo[] = [];
let nextId = 1;

// Create Elysia app
const app = new Elysia()
  // Get all todos
  .get('/todos', () => todos)
  // Add a new todo
  .post('/todos', ({ body }) => {
    const { text } = body as { text: string };
    if (!text) return { error: 'Text is required' };
    const todo: Todo = { id: nextId++, text, completed: false };
    todos.push(todo);
    return todo;
  })
  // Toggle todo completion
  .put('/todos/:id', ({ params, body }) => {
    const id = parseInt(params.id);
    const { completed } = body as { completed: boolean };
    const todo = todos.find((t) => t.id === id);
    if (!todo) return { error: 'Todo not found' };
    todo.completed = completed;
    return todo;
  })
  // Delete a todo
  .delete('/todos/:id', ({ params }) => {
    const id = parseInt(params.id);
    const index = todos.findIndex((t) => t.id === id);
    if (index === -1) return { error: 'Todo not found' };
    const deleted = todos.splice(index, 1)[0];
    return deleted;
  });

// Start the server
app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

Step 3: Run the API

Run the server with:

bun run index.ts

Your API is now live at http://localhost:3000. You can test it with tools like Postman or curl:

  • Get all todos: curl http://localhost:3000/todos
  • Add a todo: curl -X POST -H "Content-Type: application/json" -d '{"text":"Buy groceries"}' http://localhost:3000/todos
  • Toggle completion: curl -X PUT -H "Content-Type: application/json" -d '{"completed":true}' http://localhost:3000/todos/1
  • Delete a todo: curl -X DELETE http://localhost:3000/todos/1

Step 4: Add TypeScript Types

Elysia’s TypeScript support makes it easy to add type safety. For example, you can define the shape of the request body:

.post('/todos', ({ body }) => {
  const { text } = body as { text: string };
  if (!text) return { error: 'Text is required' };
  const todo: Todo = { id: nextId++, text, completed: false };
  todos.push(todo);
  return todo;
}, {
  body: t.Object({
    text: t.String()
  })
});

This ensures the text field is a string, catching errors at compile time.

Scaling the API

For a production app, you’d want to add:

  • Database Integration: Replace the in-memory todos array with a database like PostgreSQL using Prisma or Drizzle.
  • Authentication: Add JWT or OAuth for secure access.
  • Rate Limiting: Use Elysia’s middleware to prevent abuse.
  • Error Handling: Create global error handlers for consistent responses.

Elysia makes these additions simple with its plugin system and middleware support. For example, to add rate limiting:

import { rateLimit } from 'elysia-rate-limit';

app.use(rateLimit({
  max: 100,
  duration: 60 * 1000 // 100 requests per minute
}));

Why I Love This Stack

After using Bun and Elysia, I’m hooked. Bun’s speed means my APIs respond faster than with traditional Node.js setups. Elysia’s clean syntax and TypeScript-first approach make coding a breeze, and its plugin ecosystem lets me add features without bloating the codebase. Plus, Bun’s all-in-one toolkit (no need for npm or Jest) simplifies my workflow.

Conclusion

Bun and Elysia are a fantastic pair for building modern, scalable APIs. Whether you’re creating a small prototype or a large-scale app, this stack offers speed, simplicity, and flexibility. Try the example above, experiment with Elysia’s features, and see how it fits into your next project. I’d love to hear your thoughts—drop a comment or share your own Bun and Elysia tips!

Related Posts