How to Write an Express-like API Using Bun.js | by Jimmy leo | Aug, 2022

Working with the new runtime

I came across Youtube to find some interesting tech videos, then I found a video talking about Bun, which is a JavaScript runtime written in Zig. Its itself as an incredibly fast runtime, way faster than Nodejs.

So I decided to give it a try.

Here is how to download bun using the command line:

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

Based on bun’s documentation, this is how you can start an HTTP server from your index.js file:

export default {
fetch(req) {
return new Response("HI!");
},
};

In the above code, if the JavaScript file exports a default object with fetch function in it, it will start the server.

Or you can use Bun.serve:

Bun.serve({
fetch(req) {
return new Response("HI!");
},
});

To run the JavaScript file, you can simply execute bun run index.js.

And that’s it! Everything is working…

Photo by James Harrison on Unsplash

However, this API can be a little bit complicated. When the project gets bigger, we need a good way to wrap things up. So, I decided to make an express-like API using bun from scratch. In doing so, anyone who is familiar with ExpressJS is able to jump into bun quickly.

First, we need to handle the request.

We’ll create a BunServer class with an HTTP method in it:

On the HTTP request method function, the first argument path is the path of the request, second is an array of callbacks for handling the request.

Everything goes through the HTTP method will use delegate function to handle.

  • requestMap stores the map of the path and its handler is the user’s business logic to handle the request.
  • middlewares stores all the middlewares that the user declares.

Above is the delegate function, which we store them based on their request method and path and the user’s handler function. So every time user calls the path, we are able to find their corresponding handler.

For instance, we call this API:

curl -X POST 'http://localhost:3000/test

In our requestMap the key will become POST:/test the value is the handler function like (req, res) => { console.log('Donate me pls'); }

To be noted, the functions before the last function inside handlers is middlewares the last function is the actual request handler function.

And that’s how we handler request, pretty simple.

Next is the middleware.

On express, we have a middleware function with signature (req, res, next) it’s a chain of responsibility design pattern.

My implementation is quite simple here, too.

chain.js

I created a Chain function to handle middleware passing. We have three arguments in our function.

request is the bun request object, we get it from fetch(req) function.

res is the BunResponse object. On bun, we have response object, in order to return response to the client, we need to call

return new Response('hello world', { status: 200 });

But on Express, we return the response by calling:

res.status(200).send('hello world');

In order to achieve this syntax, I create a new class called BunResponse:

bun-response.js

Then we create a response object by calling its default constructor:

const res = new BunResponse();

But here is the problem, when a user accidentally calls response twice:

res.status(200).send();
res.status(500).send();

In our implementation, the first response will be ignored. This is not how ExpressJs deals with this. On Express, when we call twice for response, it will throw an error to forbid this behavior.

So here, we will do the same thing.

I create a proxy object wrapper with BunResponse object

In this term, when user calls json or send the terminate keyword, it will check if the previous Bun Response exists, if it does, it means it has been called before, we throw a new error.

And we get the response by calling

const res = responseProxy();

The third argument inside Chain is middlewares it’s the handler function for middleware.

The first line inside the Chain function is to traverse all handler function into a plain callback function with the return value res.isReady().

this.middlewares = middlewares.map((mid) => {        
return () => {
mid.middlewareFunc(req, res, this.next);
return res.isReady();
}
});

res.isReady() is to check if the user has send a stop signal. For instance, user calls res.send('return') inside the middleware. In this case, we will not continue to pass thing into the next middleware, instead, we return the response.

this.next = () => {        
if (this.isFinish()) {
return;
}
const cur = this.middlewares[this.index++];
this.isReady = cur(); if (this.isReady) {
return;
}
}

next function is similar to Express. After we call next we will go to the next middleware.

Just a few things you need to know here.

  1. every next function is called, would do index++ we get the specific callback function from middlewares with that index.
  2. Stop signal. To stop the middleware from passing, we check if this.isFinish() , which means if the index equals to the middlewares’ size. Then this.isReady() if user calls res.send('') .

Now, we can use the middlewares inside fetch function

On this above code, we check all the middlewares with default path / .

We use Chain as a constructor to create a chain object, and use next to iterate the middlewares.

After the recursion, we check if the response is ready, if it’s ready, which means the user has triggered the stop signal , we return the response immediately. If not, we check if the user has finished passing the middleware.

After that, we can use the middleware like on Express

server.use((req, res, next) => {  console.log('hello');  next();
});

Next, we need to find the middleware with the specific path. For instance, if we add a middleware on path /test we need to call this middleware before handling the request.

We start from the end of the middlewares array to check if the req.path meets. If req.path equals to path store in the middleware, we push to a new array. After the iteration, we do the same thing as above to call the middleware function.

The final thing is to handle the user logic, which we store them inside the requestMap .

if (handler) {                    
handler.apply(null, [req, res]);
}

Just that simple.

Finally, we wrap things up.

Leave a Comment