Getting Started With Bun and MongoDB
Rate this quickstart
JavaScript has come a long way since its creation in the 1990s. It's now much more than a front-end programming language used to create fancy effects on a webpage. It has matured and is now an enterprise-grade back-end language with Node.js.
In the last few years, some alternatives for using JavaScript on the back end have emerged. In addition to Node.js, runtimes such as Deno aim to fix some of the issues engineers have with JavaScript.
This article will focus on Bun, a new generation runtime for running JavaScript or TypeScript on a server.
Bun is a new runtime designed for speed. It is optimized for large-scale web servers and includes modern tooling such as a built-in TypeScript interpreter, test runner, and package manager.
It is fully compatible with Node.js, and the packages you use with your other projects should work with Bun just as well.
In this quickstart, we'll introduce you to using Bun to write a simple CRUD API to read and write from a MongoDB collection. This simple guide uses the built-in packages from Bun to create a web server and the MongoDB Node.js driver to connect to the database.
- MongoDB: Use your own MongoDB instance or create a free-forever Atlas cluster
- (Optional) Sample dataset: If you want to start with some data, you can import the sample dataset on your free Atlas cluster
If you want to start with a MongoDB and Bun template, you can run the following commands.
1 git clone https://github.com/mongodb-developer/bun-with-mongodb 2 cd bun-with-mongodb 3 bun i 4 echo "MONGODB_URI=<your_atlas_connection_string>" > .env 5 bun run index.ts
Like in a typical Node.js project, you will need to start by initializing your application. This will create the necessary scaffolding for your application.
We will keep our project very simple for this tutorial and use the built-in
Bun.serve
web server. The only dependency required is the mongodb
package from npm.In a new folder, run the following commands to initialize your project and install the required dependencies.
1 bun init 2 bun add mongodb
If you look at the files in your folder, you should now see an
index.ts
file, along with the dependencies installed in the node_modules
folder. If you are a Node.js developer, this should seem very familiar.Our application will be a simple API with five basic routes.
- POST /movies: Will take a movie in the body and insert it in the collection
- GET /movies: Retrieves the latest 10 movies that were added to the database
- GET /movies/:id: Returns a single movie by id
- PUT /movies/:id: Updates the movie specified in the path with the body
- DELETE /movies/:id: Deletes the movie specified in the path
The routes will all be handled in the main
index.ts
file. The logic to connect to the database is located in the utils/db.ts
file, and all the actions that are performed on the database can be found in the contollers/movies.ts
file.Let's start with a basic web server. Bun has multiple built-in packages, including
Bun.serve
. This package contains all the necessary components to build a basic web server.Replace the content of the
index.ts
file with the following code:1 const server = Bun.serve({ 2 async fetch(req) { 3 const url = new URL(req.url); 4 const method = req.method; 5 if (url.pathname === "/") return new Response("Welcome to the movie database"); 6 return new Response("404!"); 7 }, 8 }); 9 console.log(`Listening on http://localhost:${server.port} ...`);
Now, start the application with the following command.
1 bun --watch run index.ts
Using
--watch
will automatically reload the server whenever a file changes. This is very convenient while you're in development mode.By default, Bun will use port 3000 with
Bun.serve
. You can change that by setting your shell's PORT
environment variable. Bun will automatically use the port defined in that environment variable.To try it out, use Postman or a similar service, your web browser, or a CLI tool such as curl. For this article, I'll use curl.
Run the following command in a new terminal window to test your application.
1 curl localhost:3000
You should see a message saying Welcome to the movie database.
Congrats! You have successfully built a web server with Bun --- time to connect to a MongoDB database.
The connection to the MongoDB database will be handled by a file called
utils/db.ts
. Here, you will use the MongoDB driver (already installed with bun add
) to create a MongoClient
. You will then connect to the movies
collection and export that collection for your controllers to use.In a production environment, you'd also add some logic to ensure that the database connection is working properly, but for the purpose of this article, we'll stick to the basics.
Start with a new file called
utils/db.ts
.1 import { MongoClient } from "mongodb"; 2 let MONGODB_URI = process.env.MONGODB_URI; 3 if (!MONGODB_URI) { 4 throw new Error("Please define the MONGODB_URI environment variable inside .env"); 5 } 6 7 const client: MongoClient = await MongoClient.connect(MONGODB_URI); 8 const moviesCollection = client.db("sample_mflix").collection("movies"); 9 10 export { 11 moviesCollection 12 }
You will notice that the connection string to MongoDB (
MONGODB_URI
) is read directly from the environment variables. With Bun, no need to use a package to inject those variables into your application. It will automatically read from your .env
file.So, let's go ahead and create a
.env
file in your project's root folder.1 MONGODB_URI=<your_atlas_connection_string>
Great work! You can now connect to the database. The code isn't invoked anywhere, so there isn't much to test. Let's see how we can use this collection to perform CRUD operations.
Bun uses TypeScript out of the box. You could also decide to use plain JavaScript, but since we're leveraging TS for this application, we'll need to create a movie model to tell our application what a movie looks like. For this quickstart, our movies will only have a title, some actors, and the year in which they were released.
Create a
models/movies.ts
file:1 import type { ObjectId } from "mongodb"; 2 3 // Create a custom type for our movies 4 export interface Movie { 5 _id?: ObjectId, 6 title: string, 7 actors: string[], 8 year?: number, 9 }
Note how we're also using the native MongoDB ObjectId type here for the
_id
field.Because MongoDB is a database, your documents can contain many different properties, including arrays of strings, as we do in this example. We're keeping things simple here, but it could even be more complex objects, such as an array of actor objects. You can find out more about the document model on MongoDB University.
Now that you have your model, you can go ahead and create your movies controller.
You have a client connected to your database and a model that manages the format of your data. It's now time to do those CRUD operations on your database.
These operations are all handled in the movies controller. This controller communicates with the database and returns the operation's result to our server.
Create a new file called
controllers/movies.ts
1 // Import the Movies collection 2 import { moviesCollection } from '../utils/db.ts'; 3 // Import the necessary types 4 import type { ObjectId } from "mongodb"; 5 import type { Movie } from "../models/movies.ts"; 6 7 class MovieController { 8 /** 9 * CRUD operations for the movies collection 10 */ 11 12 // Add a movie 13 public async addMovie(movie: Movie) { 14 return await moviesCollection.insertOne(movie); 15 } 16 17 // Fetch the latest ten movies 18 public async getMovies() { 19 return await moviesCollection.find({}).sort({_id: -1}).limit(10).toArray(); 20 } 21 22 // Fetch one movie 23 public async getMovieById(_id: ObjectId) { 24 return await moviesCollection.findOne({_id}); 25 } 26 27 // Update the movies 28 public async updateMovie(_id: ObjectId, movie: Movie) { 29 return await moviesCollection.updateOne({ _id }, { $set: movie }); 30 } 31 32 // Delete a single movie 33 public async deleteMovie(movieId: ObjectId) { 34 return await moviesCollection.deleteOne({ _id: movieId }); 35 } 36 } 37 38 export default MovieController;
This controller lists the most basic operations that can be performed on the database. If you want to learn more about those operations, I recommend the MongoDB and Node.js Tutorial - CRUD Operations on Developer Center. You can also look at our docs for the find, findOne, insertOne, updateOne, and deleteOne methods from our driver in our docs.
If you need more advanced operations, such as grouping or faceting, you should also look at the MongoDB Aggregation framework.
Now that you have all the code in place to perform your CRUD operations on your collection, it's time to return to the server and start with the request routing.
It's now time to tie everything together in our
index.ts
file which is the code for the server itself. In this file, we'll import the necessary components, perform some routing, and call the appropriate methods from our controller.First, import the necessary files at the top of the
index.ts
file.1 // Import the Movies functions 2 import { ObjectId } from "mongodb"; 3 import MovieController from "./controllers/movies.ts"; 4 import type { Movie } from "./models/movies.ts";
This will import the movie controller we created earlier and the necessary types we'll use.
Next up, rewrite your
Bun.serve
logic to support various routes.1 const server = Bun.serve({ 2 async fetch(req) { 3 const url = new URL(req.url); 4 const method = req.method; 5 if (url.pathname === "/") return new Response("Welcome to the movie database"); 6 7 // Routes for the API 8 let moviesRoutes = new RegExp(/^\/movies\/?(.*)/) 9 const movies = new MovieController(); 10 11 // POST /movies 12 if (url.pathname.match(moviesRoutes) && method === "POST") { 13 return new Response("Not implemented yet!"); 14 } 15 16 // GET /movies and GET /movies/:id 17 if (url.pathname.match(moviesRoutes) && method === "GET") { 18 return new Response("Not implemented yet!"); 19 } 20 21 // PUT /movies/:id 22 if (url.pathname.match(moviesRoutes) && method === "PUT") { 23 return new Response("Not implemented yet!"); 24 } 25 26 // DELETE /movies/:id 27 28 if (url.pathname.match(moviesRoutes) && method === "DELETE") { 29 return new Response("Not implemented yet!"); 30 } 31 32 return new Response("404!"); 33 }, 34 });
In this case, we write a regular expression matching any route that starts with
/movies
. We also look at the method sent in the request. If the route doesn't match one of the handle routes or starts with /movies
, we return the message 404!
.You can test these routes using curl.
1 curl localhost:3000/movies 2 curl localhost:3000/movies -X POST 3 curl localhost:3000/movies -X PUT 4 curl localhost:3000/movies -X DELETE 5 curl localhost:3000/movies -X PATCH # 404!
Now's the time to connect your server to your database.
We can finally put everything together.
To add a movie to the database, you must read the body of the request using
req.json()
and send this movie to your controller.1 // POST /movies 2 if (url.pathname.match(moviesRoutes) && method === "POST") { 3 const movie: Movie = await req.json(); 4 return Response.json(await movies.addMovie(movie)); 5 }
Here, we're assuming that the request always matches a movie object, but in reality, you'd want to validate this. You would also likely want to handle any errors when inserting the data.
Once the code is in place, you can add a new movie to the collection.
1 curl localhost:3000/movies -X POST --data '{"title": "New Movie"}'
You should see an acknowledgement that the operation worked and receive the new
insertedId
.Our
GET /movies
route is slightly more complex as it will handle both /movies
to retrieve a list of 10 movies and /movies/:id
to retrieve a single movie. To figure out which one to use, we'll look for the existence of a parameter after the /movies
component.If it's there, we'll take this string and convert it to an ObjectId before sending it to the movies controller. If the parameter isn't there, we use the
getMovies
method from the movies controller to return the latest 10.1 // GET /movies and GET /movies/:id 2 if (url.pathname.match(moviesRoutes) && method === "GET") { 3 const routeParams = url.pathname.split("/"); 4 if (routeParams[2]) { 5 const movieId: ObjectId = new ObjectId(routeParams[2]); 6 return Response.json(await movies.getMovieById(movieId)); 7 } else { 8 return Response.json(await movies.getMovies()); 9 } 10 }
You can now test those new routes. Note that you'll need to change the route of the second curl command to match an ObjectId from your existing movie collection.
1 curl localhost:3000/movies 2 curl localhost:3000/movies/668e855f76f86976e4045ae9
We use the same trick to update a movie to find the id parameter and convert it to an ObjectId. We then pass the body of the request to the update method from our movies controller.
1 // PUT /movies/:id 2 if (url.pathname.match(moviesRoutes) && method === "PUT") { 3 const movieId: ObjectId = new ObjectId(url.pathname.split("/")[2]); 4 const movie: Movie = await req.json(); 5 return Response.json(await movies.updateMovie(movieId, movie)); 6 }
Finally, to delete a movie, we extract the movie id from the pathname and pass it to our movie controller's delete function.
1 // DELETE /movies/:id 2 if (url.pathname.match(moviesRoutes) && method === "DELETE") { 3 const movieId: ObjectId = new ObjectId(url.pathname.split("/")[2]); 4 return Response.json(await movies.deleteMovie(movieId)); 5 }
That's it! You have a fully functional server running on Bun that can connect to your MongoDB collection and perform basic CRUD operations. You can find all the code in our GitHub repository.
To deploy this code into production, you'd want to add more robustness to your code. You would also need to handle errors and return the appropriate error codes when a document is not found. But this is a good starting point for your first application.
If you have any questions, please register to our community forums, and use the form below to contact us!
Top Comments in Forums
There are no comments on this article yet.