DEV Community

Cover image for Your first API with Bun, Express and Prisma
Clerivaldo Junior
Clerivaldo Junior

Posted on

Your first API with Bun, Express and Prisma

Have you seen this new, cool, and fast runtime for JavaScript, and are wondering how to begin developing web applications? Maybe this article will help. I love to see new ways to create applications that bring innovation to the JS ecosystem, and Bun brings a little more to it. Here, without further libraries, you can create your API, test it, bundle it, and even use its own SQLite integration, all of that while being a fast and easy-to-use runtime. It even has some frameworks already, but it's content for the future.

Installation and Hello World

First of all, download and install bun using curl just like said in bun.sh

☁ ~ curl -fsSL 'https://bun.sh/install' | bash
######################################################################## 100,0%
bun was installed successfully to ~/.bun/bin/bun 
Run 'bun --help' to get started
Enter fullscreen mode Exit fullscreen mode

Then, create a folder where you want your project to be, cd into it, and execute bun init , this will scaffold a new project, you'll have to choose the project's name and the entry point. By default the cli will use the name of your folder and start at index.ts.

☁  projects  mkdir bunApp  
☁  projects  cd bunApp  
☁  bunApp  bun init    
bun init helps you get started with a minimal project and tries to guess sensible defaults. Press ^C anytime to quit

package name (bunapp): 
entry point (index.ts): 

Done! A package.json file was saved in the current directory.
 + index.ts
 + .gitignore
 + tsconfig.json (for editor auto-complete)
 + README.md

To get started, run:
  bun run index.ts
☁  bunApp  
Enter fullscreen mode Exit fullscreen mode

After that, open your favorite ide(here im using vscode) and you'll see a very lean content, with some configuration files and a index.ts containing our Hello World!!
IDE screen showing the file structure on the left and index file on the right
Almost all of those files are really common to every repository, but there's one called bun.lockb, its an auto-generated file similar to others .lock files and that's not of so much importance right now, but you can learn about it in the Bun documentation.
We already can run our index.ts file to start our little project,

☁  bunApp  bun index.ts
Hello via Bun!
☁  bunApp  
Enter fullscreen mode Exit fullscreen mode

Before we continue to the next topic, there's one more thing to do. If you're familiar with Node, you've probably used Nodemon to monitor the project and reload when the code is changed. Bun simply uses the --watch tag to run in this mode, so you don't need an external module.
Let's add two scripts to our package.json, one to start the project and one for the developer mode, including the --watch tag.

{
    "name": "bunapp",
    "module": "index.ts",
    "type": "module",
    "scripts": {
        "start": "bun run index.ts",
        "dev": "bun --watch run index.ts"
    },
    "devDependencies": {
        "bun-types": "latest"
    },
    "peerDependencies": {
        "typescript": "^5.0.0"
    },
}
Enter fullscreen mode Exit fullscreen mode

Routes

To initiate our server we just use Bun.serve(). It can receive some parameters, but for now we only need the port to access our application and the fetch() handler which we're using to deal with our requests. Write the code below and run our script bun run dev

const server = Bun.serve({
    port: 8080,
    fetch(req) {
        return new Response("Bun!")
    }
})

console.log(`Listening on ${server.hostname}: ${server.port}...`)
Enter fullscreen mode Exit fullscreen mode

That's our first http request. As we didn't specified the method, it will return the response Bun! to any request made for our endpoint, which should be localhost:8080. We will deal with it in the next topic, for now lets just add some more code following the documentation example to compose our routes.

const server = Bun.serve({
    port: 8080,
    fetch(req) {
        const url = new URL(req.url)
        if(url.pathname === "/") return new Response("Home Page!")
        if(url.pathname === "/blog") return new Response("Blog!")
        return new Response("404!")
    }
})

console.log(`Listening on ${server.hostname}: ${server.port}...`)
Enter fullscreen mode Exit fullscreen mode

It's taking our url from the request object and parsing to a URL object using Node's API. It happens because Bun aims for Node full compatibility, so most of libs and packages used on Node works on Bun out of the box.

HTTP Requests

If you wish, use console.log(req) to view our request object, it looks like this:

Listening on localhost: 8080...
Request (0 KB) {
  method: "GET",
  url: "http://localhost:8080/",
  headers: Headers {
    "host": "localhost:8080",
    "connection": "keep-alive",
    "upgrade-insecure-requests": "1",
    "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36",
    "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
    "accept-language": "en-US,en",
    "sec-fetch-mode": "navigate",
    "sec-fetch-dest": "document",
    "accept-encoding": "gzip, deflate, br",
    "if-none-match": "W/\"b-f4FzwVt2eK0ePdTZJcUnF/0T+Zw\"",
    "sec-ch-ua": "\"Brave\";v=\"117\", \"Not;A=Brand\";v=\"8\", \"Chromium\";v=\"117\"",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": "\"Linux\"",
    "sec-gpc": "1",
    "sec-fetch-site": "none",
    "sec-fetch-user": "?1"
  }
}
Enter fullscreen mode Exit fullscreen mode

Now here's the thing, we can use a LOT of conditionals to check the method and/or the endpoint. It becomes painfully polluted and doesn't look good to read

const server = Bun.serve({
    port: 8080,
    fetch(req) {
        const url = new URL(req.url)
        if(url.pathname === "/") return new Response("Home Page!")
        if(url.pathname === "/blog") {
        switch (req.method) {
            case "GET":
            // handle with GET
            case "POST":
            // handle with POST
            case "PUT":
            // handle with PUT
            case "DELETE":
            // handle with DELETE
            }
            // any other routes and methods...  
        }
        return new Response("404!")
    }
})

console.log(`Listening on ${server.hostname}: ${server.port}...`)
Enter fullscreen mode Exit fullscreen mode

Remember that most Node packages runs on Bun as well? Let's use Express to ease our development process, we can view details in Bun's documentation. Let's start by stopping our application with CTRL + C and running bun add express

Listening on localhost: 8080...
^C
☁  bunApp  bun add express
bun add v1.0.2 (37edd5a6)

 installed express@4.18.2


 58 packages installed [1200.00ms]
☁  bunApp 
Enter fullscreen mode Exit fullscreen mode

and rewrite our index.ts using a Express template with some routes

import express, { Request, Response } from "express";

const app = express();
const port = 8080;
app.use(express.json());

app.post("/blog", (req: Request, res: Response) => {
//create new blog post
});

app.get("/", (req: Request, res: Response) => {
res.send("Api running");
});

app.get("/blog", (req: Request, res: Response) => {
//get all posts
});

app.get("/blog/:post", (req: Request, res: Response) => {
//get a specific post
});

app.delete("/blog/:post", (req: Request, res: Response) => {
//delete a post
});

app.listen(port, () => {
console.log(`Listening on port ${port}...`);
});
Enter fullscreen mode Exit fullscreen mode

Adding a database

One last thing for our CRUD is to implement Database connections. Bun already has its own SQLite3 driver, but we're using Prisma since dealing with an ORM is easier. Let's follow the guide from Bun documentation and start by adding Prisma with bun add prisma and initializing it with bunx prisma init --datasource-provider sqlite. Then navigate to our new schema.prisma file and insert a new model.

☁  bunApp  bun add prisma                               
bun add v1.0.2 (37edd5a6)

 installed prisma@5.3.1 with binaries:
  - prisma


 2 packages installed [113.00ms]
☁  bunApp  bunx prisma init --datasource-provider sqlite

✔ Your Prisma schema was created at prisma/schema.prisma
  You can now open it in your favorite editor.

warn You already have a .gitignore file. Don't forget to add `.env` in it to not commit any private information.

Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Run prisma db pull to turn your database schema into a Prisma schema.
3. Run prisma generate to generate the Prisma Client. You can then start querying your database.

More information in our documentation:
https://pris.ly/d/getting-started

☁  bunApp  
Enter fullscreen mode Exit fullscreen mode

After that, run bunx prisma generate and then bunx prisma migrate dev --name init. Now we got what we need to our little API. Go back to our index.ts, import and initialize the Prisma client, then we're ready to finish our routes.

import { PrismaClient } from "@prisma/client";

/* Config database */
const prisma = new PrismaClient();
Enter fullscreen mode Exit fullscreen mode

The final index.ts file should look like this in the end:

import express, { Request, Response } from "express";
import { PrismaClient } from "@prisma/client";

/* Config database */
const prisma = new PrismaClient();

/* Config server */
const app = express();
const port = 8080;
app.use(express.json());

app.post("/blog", async (req: Request, res: Response) => {
    try {
        const { title, content } = req.body;

        await prisma.post.create({
            data: {
            title: title,
            content: content,
            },
        });
        res.status(201).json({ message: `Post created!` });
    } catch (error) {
        console.error(`Something went wrong while create a new post: `, error);
    }
});

app.get("/", (req: Request, res: Response) => {
    res.send("Api running");
});

app.get("/blog", async (req: Request, res: Response) => {
    try {
        const posts = await prisma.post.findMany();
        res.json(posts);
    } catch (error) {
        console.error(`Something went wrong while fetching all posts: `, error);
    }
});

app.get("/blog/:postId", async (req: Request, res: Response) => {
    try {
        const postId = parseInt(req.params.postId, 10);

        const post = await prisma.post.findUnique({
            where: {
                id: postId,
            },
        });
        if (!post) res.status(404).json({ message: "Post not found" });
        res.json(post);
    } catch (error) {
        console.error(`Something went wrong while fetching the post: `, error);
    }
});

app.delete("/blog/:postId", async (req: Request, res: Response) => {
    try {
        const postId = parseInt(req.params.postId, 10);
        await prisma.post.delete({
            where: {
            id: postId,
            },
        });
        res.send(`Post deleted!`);
    } catch (error) {
        return res.status(404).json({ message: "Post not found" });
    }
});

app.listen(port, () => {
    console.log(`Listening on port ${port}...`);
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

Finally our API is done! From here on you can implement a bunch of other things such as: adding more models, creating a frontend and, of course, writing some tests(try do that next). You can see that Bun provides some quality of life while being compatible with most of Node packages, making our developer lives easier.

Top comments (6)

Collapse
 
danielhe4rt profile image
Daniel Reis

Awesome article! I didn't had time to read about this Bun fever until now.

Thanks for the insights!

Collapse
 
dannielrm profile image
Danniel Rodrigues

Loved how you baby steps through the process. It's nice to have Node modules from the get go. Do you have any benchmark data agains other runtime options?

Collapse
 
clerijr profile image
Clerivaldo Junior

I do not have an in-depth benchmark. You can see some comparisons on Bun's homepage, but I've heard that it's faster for some and slower for others, depending on how you use it.

Collapse
 
henriqueleme profile image
Henrique Leme de Brito

Amazing explanation, I've read about Bun but didn't have time to see any code yet, is great to know how it works

Collapse
 
daniele_testa profile image
Daniele Testa • Edited

I am confused. First you use the bun.Serve() which is the whole reason why Bun is fast (because it's using uWebsockets under the hood). Then you switch to using the "regular" app.listen from Express which makes it A LOT slower. Then why use Bun at all? You even chose to use an ORM over the 10x faster built-in SQLite3 library of Bun...

Collapse
 
renato_zero6 profile image
Renato Rebouças

👏🏻👏🏻👏🏻