Building a High-Performance Library Management API with Bun and Bao, Powered by Prisma, and Containerized with Docker

Building a High-Performance Library Management API with Bun and Bao, Powered by Prisma, and Containerized with Docker

The dynamic landscape of web development is constantly being reshaped by innovative tools that promise to enhance performance, improve developer productivity, and simplify the deployment process. Node.js has been a significant player in this arena, providing the backbone for server-side development for many years. However, a new challenger has emerged, one that is equipped to leverage modern advancements in technology to offer an even more streamlined and efficient development experience: Bun.

Bun, a fresh and performant JavaScript runtime, introduces a paradigm shift in building server-side applications. Alongside Bao, a routing framework for Bun reminiscent of Express, it offers developers the familiar comfort of Node.js with an amplified focus on performance.

In this guide, we'll explore the process of setting up a simple yet robust API for a library management system using Bun and Bao, utilizing Prisma as our Object-Relational Mapping (ORM) layer to interact seamlessly with a database, and Docker to containerize our setup, ensuring a consistent development and deployment environment.

As we unfold the layers of this modern tech stack, we'll not only learn how to set up and build with Bun but also understand the benefits and potential of this new ecosystem for developers looking to push the boundaries of server-side programming.

A GitHub repository is available here for you to follow along

Understanding Bun

A Brief History of Bun

Bun originated from the need for a runtime that could match the evolving JavaScript ecosystem's pace while addressing the performance bottlenecks often encountered in Node.js. With the intention to make JavaScript server-side development as performant as possible, Bun was engineered from the ground up, employing cutting-edge programming languages and compilation techniques.

Key Features of Bun

Bun's innovative features include:

  • JIT Compilation: Utilizing the Zig programming language for its JIT compilation process, Bun optimizes the performance of JavaScript code by compiling it just in time for execution.
  • Native Package Management: Bun integrates a native package manager that works seamlessly with the npm registry, simplifying package installation and management without the need for additional tools.
  • Web API Implementations: By adopting Web API standards, Bun allows developers to use familiar browser APIs right within the server environment, providing a unified coding experience.

Comparing Bun to Node.js

Node.js has the advantage of time, with a vast and deeply-rooted ecosystem, community support, and a plethora of packages and tools that developers have come to rely on. On the other hand, Bun shines in performance metrics, offering impressive benchmarks that include faster request handling and reduced startup times—key factors in the decision-making process for developers prioritizing performance in their applications.

In the next section, we'll begin our practical journey with Bun, starting from installation and moving on to project setup. We'll learn how to structure a Bun application in a way that promotes maintainability and scalability, two aspects that are crucial for any serious development project.

Setting Up Our Development Environment with Bun and Bao

Installing Bun

To dive into the world of Bun, we first need to get it up and running on our machines. Installation is straightforward:

  • On macOS and Linux, you can install Bun using the following shell command:
curl https://bun.sh/install | bash
  • For Windows users, Bun provides an installer that can be downloaded from its official website.

With Bun installed, you can verify the installation by running:

bun --version

This command should return the version of Bun currently installed on your system.

Creating Our Project

Now that we have Bun ready, let’s create a new directory for our library management system:

mkdir library-management-api
cd library-management-api

We can initialize our project with:

bun init

This will generate a package.json file, marking the start of our Bun project.

Integrating Bao for Routing

Bao offers a minimalist routing solution for Bun applications, modelled after Express. To install Bao, use Bun’s package manager:

bun add baojs

Structuring the Project

In a modern development environment, organizing our project into feature folders allows us to isolate different aspects of the API, making it easier to maintain and scale. Here's how we might structure our library management system:

/library-management-api
|-- /src
    |-- /books
        |-- books.controller.ts
        |-- books.service.ts
        |-- books.router.ts
    |-- /users
        |-- users.controller.ts
        |-- users.service.ts
        |-- users.router.ts
    |-- app.ts
|-- .env
|-- docker-compose.yml
|-- Dockerfile
|-- package.json

In this structure, each domain of our application, such as books and users, has its own set of files for handling different responsibilities (following the MVC pattern). The app.ts file will serve as the entry point of our application, where we'll set up Bao and integrate our routes.

Setting Up Our First Route with Bao

Here’s a simple example to set up our first route using Bao in app.ts:

import Bao from "baojs";

const app = new Bao();

app.get("/", (req, res) => {
  res.send("Welcome to the Library Management API!");
});

const server = app.listen({
  port: 3000,
  hostname: "0.0.0.0",
});

console.log(`Library API is running on ${server.hostname}:${server.port}`);

Testing Our API

To test if everything is working as expected, we can start our server with:

bun run src/app.ts

Navigating to http://localhost:3000 in a browser or using a tool like curl, we should be greeted with our welcome message.

Effective Routing with Bao

Routing is a crucial component of any web server framework, dictating how the application responds to client requests to various endpoints. In Bun, Bao serves a similar purpose to Express in Node.js environments, offering a simplified and familiar approach to setting up routes. Let's delve into how we can leverage Bao to construct a well-organized routing system.

Modular Routing with Feature Folders

In a modular system, different aspects of the API are isolated within feature folders. Each feature, such as books or users, has its own dedicated folder containing its routes, controllers, and any other necessary files.

For example, a books feature folder might contain:

  • books.router.ts – Defines routes specific to books.
  • books.controller.ts – Contains functions that the router calls to process requests.
  • books.service.ts – Holds business logic, abstracted away from controllers for cleaner code.

The same structure would be replicated for users and any other feature modules you need.

books.router.ts would look something like this:

import { Bao } from "baojs";
import { createBook, getBooks } from "./books.controller";

export const Router = (app: Bao) => {
  app.get("/books", getBooks);
  app.post("/books", createBook);

  // More route definitions...
}

And you would similarly set up users.router.ts for user-related operations.

The final step is to integrate these feature routers into your main application file.

app.ts

import { Bao } from "bao";
import { Router as BookRouter } from "./books/books.router";

const app = new Bao();

BookRouter(app);

// Remaining app setup...

With this approach, you maintain a clean and scalable codebase, with each domain concern neatly compartmentalized into its own feature module. This setup not only enhances readability but also simplifies maintenance and testing.

Leveraging Prisma for Database Management

In our journey to craft a simple yet robust API using Bun and Bao, we encounter the necessity for a reliable database management tool. Prisma emerges as the ideal candidate with its comprehensive ORM capabilities designed for TypeScript and Node.js. It simplifies database operations, making them more maintainable and less error-prone.

Prisma's Role in our API

Prisma functions as the intermediary between our TypeScript code and the database, translating our object-oriented operations into efficient SQL queries. For our library management system, this is invaluable, allowing us to define and manipulate our data models with ease and precision.

Setting Up Prisma

Installation

bun add prisma

The Prisma CLI is a development dependency that enables us to initialize and manage our Prisma setup.

Initialization

bun run prisma init

This command scaffolds the necessary configuration files, notably the prisma/schema.prisma file where we define our data models.

Modelling Data

In schema.prisma, we craft the blueprint of our library's database schema using Prisma's schema language. For instance:

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Book {
  id        Int      @id @default(autoincrement())
  title     String
  author    String
  isbn      String   @unique
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

The DATABASE_URL is an environment variable that will hold our database connection string, separating configuration from code for better security and flexibility.

Migrations

With our models in place, we generate and apply migrations to update the database structure:

npx prisma migrate dev --name init

Interaction with TypeScript

Prisma Client's auto-generated query builder meshes seamlessly with TypeScript, granting us type safety and autocompletion. Here's how we might create a book entry:

import { Book,PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

async function createBook(): Promise<Book> {
  return await prisma.book.create({
    data: {
      title: '1984',
      author: 'George Orwell',
      isbn: '123-456-789'
    },
  });
}

Prisma ensures our interactions with the database are straightforward and efficient. It handles connections and transactions with ease, letting us focus on implementing features for our library management system without sweating over database complexities.

Leveraging Docker for Development and Production

Utilizing Docker offers a multitude of benefits for both development and deployment stages. For developers, Docker ensures a consistent environment that is isolated from individual configurations and dependencies, eliminating the "it works on my machine" problem. When it comes to deployment, Docker containers can be easily shipped to production environments, ensuring that the software operates identically in both development and production.

Dockerizing our API

By creating a Dockerfile, we instruct Docker on how to build our application's image:

# Use the node image (or Prisma won't work)
FROM node:18

# Install Bun
RUN npm install -g bun

# Set the working directory
WORKDIR /app

# Install dependencies
COPY package.json ./
COPY bun.lockb ./
RUN bun install

# Bundle app source
COPY . .

# Compile TypeScript and generate Prisma client
RUN bun x prisma generate
RUN bun build ./src/app.ts --outfile=app.js

# Expose the port the API listens on
EXPOSE 3000

# Start the API with Bun
CMD ["bun", "run", "app.js"]

Orchestrating Services with docker-compose.yml

The docker-compose.yml file defines our services, network, and volume configurations:

version: '3.8'

services:
  api:
    build: .
    ports:
      - "3000:3000"
    command: bun run --watch src/app.ts
    environment:
      DATABASE_URL: postgresql://prisma:prisma@db:5432/library?schema=public
    depends_on:
      - db
    volumes:
      - .:/app
      - /app/node_modules

  db:
    image: postgres:latest
    restart: always
    environment:
      POSTGRES_USER: prisma
      POSTGRES_PASSWORD: prisma
      POSTGRES_DB: library
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data/

  prisma:
    build: .
    depends_on:
      - db
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      DATABASE_URL: postgresql://prisma:prisma@db:5432/library?schema=public
    command: bun x prisma migrate deploy

volumes:
  postgres_data:

Running Prisma Migrations with Docker Compose

Applying migrations is as straightforward as running a single command:

docker-compose run prisma

Incorporating Docker into our workflow offers a reproducible environment that streamlines onboarding for new developers and reduces discrepancies between development and production. It also simplifies deployment processes, allowing for easy scaling and management of the application infrastructure. By following the above examples, we've created a robust setup where our API and database services are containerized, enabling seamless migrations and development workflows.

Communicating with the Library API

Having our API and database containerized and running smoothly with Docker is a significant milestone. It’s now time to see our simple library management system in action. We can use cURL, a command-line tool for making HTTP requests, to interact with our API endpoints.

Here’s how we can perform some basic operations:

Adding a New Book:

curl -X POST http://localhost:3000/books -H "Content-Type: application/json" -d '{"title": "Sapiens", "author": "Yuval Noah Harari", "isbn": "1846558239"}'

Retrieving All Books:

curl http://localhost:3000/books

Updating a Book:

curl -X PUT http://localhost:3000/books/1 -H "Content-Type: application/json" -d '{"title": "Homo Deus 2", "author": "Yuval Noah Harari", "isbn": "1846558239"}'

Deleting a Book:

curl -X DELETE http://localhost:3000/books/1

These cURL commands mimic the interactions that a user would have through a front-end interface or another service consuming our API.

Reflecting on the Journey

Embarking on the journey of setting up an API with Bun, Bao, and Prisma has been a testament to the powerful and evolving landscape of modern web development. Not only has the process been streamlined, but it's also been a fun learning curve. The performance gains from Bun, coupled with the familiar routing approach of Bao and the robustness of Prisma ORM, present a combination that's hard to ignore.

This guide should serve as a jumping-off point for developers to explore these exciting technologies. The simplicity and efficiency of this setup allow us to focus on the creative aspects of API development, making our work not just productive but also enjoyable.

From setting up the environment with Docker to executing database migrations and finally making requests to our API, we have seen the capabilities of these tools and how they can ease the process of creating scalable web applications. The enjoyment derived from seeing a project come together seamlessly cannot be understated—it reinforces the reason many of us became developers: to create, innovate, and solve problems in increasingly better ways.