Building Scalable APIs with Bun and Elysia: A Developer's Guide
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
todosarray 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!