Learn how to set up, develop, secure,
and test Bun CRUD app using Elysia.js, TypeScript, and MongoDB.
What is Bun?
It's promoted as a fast runtime that provides out-of-the-box TypeScript support,
the use of native Node.js & NPM modules, as well as a more unified ecosystem of tools.
At the moment Bun is only supported on Unix OS.
If you're a Windows user you can set up Bun on WSL:
NPM: npm i -g bun (does not work for Windows currently)
In this blog, we'll use Elysia, the API framework for Bun (sort of like Express to Node.js).
Although Elysia is inspired by Express, it brings interesting innovations to the formula,
some of which I'll talk about in this article.
Initialize a project
There are two ways to set up the Elysia project on your machine:
This approach revolves around you setting up the dependencies yourself,
starting with initializing the Bun project:
Then install Elysia and all required dependencies:
Then you add scripts in the package.json file to run the project:
Then you can run the app using a script bun dev
and proceed with creating your dream app.
You can skip all of these steps if you're a pre-baked template.
This is how you set an Elysia from the template:
Which should generate an Elysia app that is ready to use.
Elysia app generated using Bun template
Elysia.js syntax should be familiar to anyone who used Express.js before.
Upon importing Elysia, you create an instance of it and assign it to a new variable (app).
You'll use the app to set the route (endpoint) methods:
Each endpoint is a function that contains at one two parameters: a URI path,
followed by a callback function that may or may not contain a context handler parameter.
Should you feel the need you can prefix the handler callback function with the async keyword
in order to use the await within the callback.
Expose the Port
Just like in the Express.js app, you need to set
a port that Elysia will use to launch a server.
The server setup is completed., If you visit the app on a specific port, e.g. localhost:3000,
you should see 'Hello World' displayed in the browser.
Groups & Controllers
If you have several APIs that fall under the same category, e.g.:
You can make use of groups to better organize them and avoid repeating yourself.
Now every endpoint under this group (user) will have the same prefix.
If you'd like to split features into controllers you can do this.
First, create a function that wraps routes in some file:
Then export this function and inject it into the main router using the
use middleware function.
Now you split routes per controller.
Bun has built-in support for reading environment variables through a .env file.
Simply create a .env file in your project and let Bun handle the rest.
You can still use the good old process.env to read the environment variable:
In Elysia you no longer have request and the response objects in the route.
Instead, the two are merged into one Context Handler object
that is used to read incoming data and send back a proper response.
You can access particular properties of the request:
You can add additional features to the request handler by using the decorate router function:
Then in the callback, you can make use of the decorated value:
A common use case for this is to inject a database instance into your router
so that all routes can access it without needing to re-instantiate.
To return the response to the client you simply use the return
keyword built into the language. The route can return any type of response and
it will set the correct content type automatically:
You can even return HTML using a @elysiajs/htmlplugin
(that you need to install).
Response Headers and Status Code
But what about returning a custom status code? For this, you can use the set flag
on the Context handler that will alter the response object sent to the client.
Now the response data will have a status code 201 and a custom header alongside the response text/object.
API Validations with Guards
You can protect routes and validate incoming data using Guards.
Here, I'm validating the request body sent to this route to make sure that the consumer provides
all three required parameters.
If any of the conditions fails guard will throw a 400 Bad Request error.
Hooks are a special set of functions in Elysia that act like middlewares
that fire in between the request and the response.
You can create Hooks per route or for multiple routes.
Before & After Handle
These hooks can be used to validate the request before it reaches a certain route.
For example, you can validate if a request body contains a desired header:
OnResponse & OnError
These hooks are used when you want to intercept the response sent from the route or handle the error.
For example, you can use the OnResponse hook to log all responses:
You can find the Error Handling hook in the repository linked below.
When applying hooks to multiple routes at once, don't forget
that the hook needs to be called before the endpoint you want to apply it to:
First, install Mongoose in the project:
After the installation, create a directory in the src folder where you'll set up a MongoDB connection.
I usually create a database for free on MongoDB Cloud. However, you can also run a local instance.
Connect to MongoDB
Once you're done with the setup, put the connection string in the .env file:
Then use Mongoose to connect using the connection string:
Finally, import this database setup file into the index.ts file:
In order to work with the database data, you need to set up an entity model.
Since MongoDB is a document database, the models are created as JSON schemas.
First, set up an interface that the schema will based on.
Second, create a User schema.
I added a couple of constraints for forcing a unique username & email, making each field mandatory,
and omitting a password from the response.
Now comes to CRUD part. You set up a controller that will interact with the database
using the previously created User schema.
# Path to the Users Controller
# CRUD API
Registering the Users Controller
To put everything together, import the controller in the index.ts file
and pass the reference to the controller into the middleware.
The Users controller is ready to be used.
Another important factor of software development is testing.
When testing you should not use the production data.
Instead, tests rely on fakes, mocks, and stubs.
The database is seeded with data you can play with, without harming the real data.
Bun provides a built-in test runner that you can use to test your functions and APIs.
I updated the database setup file to use the test database
only in the testing environment (when NODE_ENV === 'test').
Test runner script
Update the package.json file add a test hook among the scripts.
The test hook will run bun test on any file that ends with
I created a separate directory “test” in the project root
that will contain all test files.
You can test each endpoint using the Request class and the Fetch API.
First of all, you need to export the app object (instance of Elysia)
in order to use it as a router within tests.
Then within tests make use of the app and Fetch API to simulate API requests:
Running Test Suite
To run the test-runner, execute the test script in the package.json file:
You can find the full test suite in the project
Thoughts on Bun
Bun is fast and fun to work with, but still a bit buggy. For example:
The editor is complaining about errors even though the compiler works fine
Mongoose throws an error while attempting to retrieve an item that does not exist
You can't use lots of packages from other frameworks like (Express, Fastify, or Nest.js)
as they work on the Request / Response model, and in Elysia,
there is only one Context handler object per endpoint
But nothing really game-breaking or that can't be fixed by a patch.
I'll definitely keep an eye on Bun and I encourage you to give it a shot.
Get Full Code
Feel free to clone the project and play around with it.