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.