Building a RESTful API with Bun and ElysiaJS

Building a RESTful API with Bun and ElysiaJS
Bun
Elysia
Rest API

Junius L

September 16, 2023

4 min read

Introduction

In this blog post, we will explore the process of building a RESTful API using Bun and ElysiaJS. Bun is an all-in-one toolkit for JavaScript and TypeScript apps. It ships as a single executable called bun, and ElysiaJS is a TypeScript framework supercharged by Bun with End-to-End Type Safety. By combining these two tools, we can create efficient and scalable RESTful APIs with ease.

Prerequisites

Before we dive into the details, make sure you have the following prerequisites installed:

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

Getting Started

Step 1: Setting up a Bun Project

First, let's create a new Bun project:

bun create elysia books

Now, navigate to the books directory and open the index.ts file. Paste the following code into it:

import { Elysia, t } from "elysia";
 
const app = new Elysia()
 
app.get('/books', () => 'books');
app.post('/books', () => 'books')
app.put('/books', () => 'books')
app.get('/books/:id', () => 'books')
app.delete('/books/:id', () => 'books')
 
app.listen(8081)
 
 
console.log(
  `🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}`
);

Run the application

Execute the following command to run your application:

bun dev

You should see the following output in your terminal:

🦊 Elysia is running at localhost:8081

Adding SQLite

Now let's add SQLite database to our application, inside src create a new file and name it db.ts and paste the following code

import { Database } from 'bun:sqlite';
 
export interface Book {
    id?: number;
    name: string;
    author: string;
}
 
export class BooksDatabase {
    private db: Database;
 
    constructor() {
        this.db = new Database('books.db');
        // Initialize the database
        this.init()
            .then(() => console.log('Database initialized'))
            .catch(console.error);
    }
 
    // Get all books
    async getBooks() {
        return this.db.query('SELECT * FROM books').all();
    }
 
    // Add a book
    async addBook(book: Book) {
        // q: Get id type safely
        return this.db.query(`INSERT INTO books (name, author) VALUES (?, ?) RETURNING id`).get(book.name, book.author) as Book;
    }
 
    // Update a book
    async updateBook(id: number, book: Book) {
        return this.db.run(`UPDATE books SET name = '${book.name}', author = '${book.author}' WHERE id = ${id}`)
    }
 
    // Delete a book
    async deleteBook(id: number) {
        return this.db.run(`DELETE FROM books WHERE id = ${id}`)
    }
 
    async getBook(id: number) {
      return this.db.query(`SELECT * FROM books WHERE id=${id}`).get() as Book;
    }
 
    // Initialize the database
    async init() {
        return this.db.run('CREATE TABLE IF NOT EXISTS books (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, author TEXT)');
    }
}

Elysia decorate

We can utilized the database in our handlers by passing it to the Elysia context using decorate

let's modify the index file and add the db to Elysia context

import { Elysia, t } from "elysia";
import { BooksDatabase } from './db/db';
 
const app = new Elysia().decorate('db', new BooksDatabase)
 
app.get('/books', ({ db }) => db.getBooks());
app.post('/books', () => 'books')
app.put('/books', () => 'books')
app.get('/books/:id', () => 'books')
app.delete('/books/:id', () => 'books')
 
app.listen(8081)
 
 
console.log(
  `🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}`
);

Now our first endpoint uses the SQLite db from the context

Elysia Schema

To define strict types for Elysia handlers, we've introduced schema usage. The schema ensures type safety for parameters like the request body

Let's modify our code to use the schema for our post and put endpoint

import { Elysia, t } from "elysia";
import { BooksDatabase } from './db/db';
 
const app = new Elysia().decorate('db', new BooksDatabase)
 
app.get('/books', ({ db }) => db.getBooks());
app.post('/books', ({db, body }) => db.addBook(body), {
  body: t.Object({
    name: t.String(),
    author: t.String()
  })
})
app.put('/books', ({ db, body }) => db.updateBook(body.id, {name: body.name, author: body.author }), {
  body: t.Object({
    id: t.Number(),
    name: t.String(),
    author: t.String()
  })
})
 
app.get('/books/:id', () => 'books')
app.delete('/books/:id', () => 'books')
 
app.listen(8081)
 
 
console.log(
  `🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}`
);

Path parameters

Elysia enables us to access path parameters using the params object from the Elysia context. We've used path parameters in the remaining endpoints for enhanced flexibility.

Let's modify the code and use the path parameters for the remaining endpoints.

import { Elysia, t } from "elysia";
import { BooksDatabase } from './db/db';
 
const app = new Elysia().decorate('db', new BooksDatabase)
 
app.get('/books', ({ db }) => db.getBooks());
app.post('/books', ({db, body }) => db.addBook(body), {
  body: t.Object({
    name: t.String(),
    author: t.String()
  })
})
app.put('/books', ({ db, body }) => db.updateBook(body.id, {name: body.name, author: body.author }), {
  body: t.Object({
    id: t.Number(),
    name: t.String(),
    author: t.String()
  })
})
 
app.get('/books/:id', ({db, params }) => db.getBook(parseInt(params.id)))
app.delete('/books/:id', ({db, params }) => db.deleteBook(parseInt(params.id)))
 
app.listen(8081)
 
 
console.log(
  `🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}`
);

Source code

https://github.com/julekgwa/Elysia-with-Bun-runtime

Conclusion

In this blog post, we've embarked on a journey to build a robust RESTful API using Bun and ElysiaJS. These tools, when combined, offer a seamless development experience with features like type safety, flexible routing, and easy database integration.