How To Build a Web API using Express.js, Bun and MongoDB?

Leverage Bun and build a high-performing Express.js API

Tharaka Romesh
Bits and Pieces

--

Web APIs serve as the central components of modern software, seamlessly connecting applications across the expansive landscape of the internet.

If you’re working with Node.js, you’re most likely familiar with the MER(A)N stack:

  1. MongoDB
  2. Express
  3. React/Angular
  4. Node

But, ever since Bun has come to the picture, building high performing APIs haven’t been challenging. So, this article will aim on building an Express.js API backed with MongoDB, powered by Bun!

Understanding the Tools

If you’re not familiar with any of these tools, don’t worry. Let’s dive into these one by one and understand what we’re going to be working with:

  • Express.js: It is a sleek and straightforward web framework seamlessly integrated with Node.js. It lets users build APIs with ease.
  • MongoDB: NoSQL database excels in versatility and effortlessly manages large data loads.
  • Bun: Bun is a JavaScript runtime that serves to be the replacement for Node.js

Step 01: Setting Up the Application and Building the API

1. Installing Bun

First and foremost, let’s install Bun! To do so, run the command:

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

You can verify the installation by running bun --version .

2. Creating a Bun Project

Let’s initialize our Bun project with:

bun create blog-api

You can adopt a folder directry structure as below:

/blog-api

├── src
│ ├── models
│ ├── controllers
│ ├── routes
│ └── index.ts

├── dist

├── node_modules

├── .bunrc
├── .env
├── package.json
└── tsconfig.json

3. Installing the libraries: Express.js, MongoDB, and Typescript

Now that you have Bun, you can install all the required packages using the Bun Package Manager itself using the commands:

bun install express mongoose 
bun install @types/express @types/mongoose typescript bun-types -d

These scripts will install Express.js for our server, MongoDB through Mongoose for the database, and Typescript to keep our code clean and safe.

4. Brewing the Basic Server Potion

Inside, src/app.ts create an Express.js server:

import express from 'express';

const app = express();

app.use(express.json());

app.get("/ping", (_, res) => {
res.send("🏓 pong!");
});

app.listen(3000, () => {
console.log('The magic happens on port 3000!');
});

This is your basic Express.js app definitition.

Now, let’s ensure that our API is spun up using Bun . We’ll do this by tweaking our npm start script in package.json:

"scripts": {
"start": "bun run src/app.ts,"
"dev": "bun run src/app.ts --watch"
}

You can kickstart your application into action with this simple command

bun dev

The --watch mode in Bun offers an ultra-fast, native file-watching experience, sharply contrasting with Node.js, which requires external tools like Nodemon to achieve similar functionality for development convenience.

You can check if the server is up and responsive by making a GET request to the /ping route, which will reply with a playful "🏓 pong!".

5. Designing the Mongoose Schema

In the src/models folder, let's create Post.ts :

import { Schema, InferSchemaType, model } from 'mongoose';

const postSchema = new Schema(
{
title: { type: String, required: true },
content: { type: String, required: true },
author: String,
createdAt: { type: Date, default: Date. now },
}
);

export type Post = InferSchemaType<typeof postSchema>;
export const Post = model('Post', postSchema);

6. Controllers: Defining Business Logic

Each route needs a corresponding controller function.

For instance, in your controllers folder, you could have postController.ts :

import { Request, Response} from 'express';

export const createPost = async (req: Request, res: Response) => {
try {
const {author, title, content} = req.body;
const post = new Post({author, title, content});
await post.save();
res.status(201).send(post);
} catch (error) {
res.status(400).send(error);
}
};

export const readPost = async (req: Request, res: Response) => {
try {
const posts = await Post.find({});
res.status(200).send(posts);
} catch (error) {
res.status(500).send(error);
}
};

export const readPosts = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const post = await Post.findById(id);
if (!post) {
res.status(404).send();
}
res.status(200).send(post);
} catch (error) {
res.status(500).send(error);
}
};

export const updatePost = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const post = await Post.findByIdAndUpdate(id,
req.body,
{
new: true,
runValidators: true
});
if (!post) {
res.status(404).send();
}
res.status(200).send(post);
} catch (error) {
res.status(400).send(error);
}
};

export const deletePost = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const post = await Post.findByIdAndDelete(id);
if (!post) {
res.status(404).send("Post wasn't found");
}
res.status(200).send(post);
} catch (error) {
res.status(500).send(error);
}
};

Remember to manage errors with care. Encase your controllers in try-catch blocks and send informative responses to the client. Subsequently, import these functions into your routes and incorporate them as needed.

7. Adding Routes to the Business Functions

For a Blog API, you’ll want these CRUD-necessary routes:

  • Create a post
  • Read a post
  • Update a post
  • Delete a post

Inside your src/routes directory, you'll create blogRoutes.ts with the following:

import express from 'express';

const router = express.Router();

import { createPost, readPost, readPosts, updatePost, deletePost } from '../controllers/postController';

router.post('/post', createPost);
router.get('/posts', readPosts);
router.get('/post/:id', readPost);
router.put('/post/:id', updatePost);
router.delete('/post/:id', deletePost);

export default router;

8. Configuring MongoDB

It’s time to bring to life the database connection. Add the following to your src/config/db.ts :

import mongoose from 'mongoose';

export default function connectDB() {

const mongoURI = process.env.MONGODB_URI ?? 'mongodb://localhost:27017/mydatabase';

try {
mongoose.connect(mongoURI);
} catch (error) {
const castedError = error as Error;
console.error(castedError.message);
process.exit(1);
}

mongoose.connection.once("open", (_) => {
console.log(`Database connected`);
});

mongoose.connection.on("error", (err) => {
console.error(`Database connection error: ${err}`);
});

}

Now, create a .env file in your project’s root and add your settings, like the MONGODB_URI, right in there.

In this configuration, we access environment variables directly without depending on utilities like dotenv, a common choice for Node.js projects. This is achievable because Bun inherently supports the exposure of these variables through .env files by default.

9. Stitching all together

After preparing your Models, Routes, and Controllers, incorporate them into the index.ts file. Following this integration, your application is primed for launch.

import express from "express";
import postRoute from "./routes/postRoute"
import connectDB from "./config/db"


const app = express();
const port = process.env.APP_PORT || 8080;

// connect to database
connectDB();

// middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.get("/ping", (_, res) => {
res.send("🏓 pong!");
});

// routes
app.use('/api/post', postRoute);

app.listen(port, () => {
console.log(Listening on port: ${port} );
});

And just like that, your API powered by Bun is ready.

Adding Monitoring to Express.js Applications

Logging requests in Express

Logging is essential for monitoring, debugging, and keeping track of application activity. Morgan is versatile and customizable, making it a valuable tool for developers looking to gain insights into their Express applications.

First, you’ll need to install Morgan. You can do this through bun:

bun add morgan
bun add @types/morgan -d

You’ll want to add Morgan as middleware in your Express application. This is done using app.use(). Morgan has various predefined logging formats like dev, tiny, combined, common, etc. You can also create your own format. Let's add a custom format to the index.ts file.

app.use(morgan(':method :url :status :response-time ms'));

Morgan is hooked up with your Express app, ready to log all those HTTP requests. Logging comes super handy for keeping an eye on things and squashing bugs. Morgan’s flexibility means you can tweak it to fit your needs perfectly.

Data validation in Express

To guarantee that the incoming data for your application is clean and structured to your specifications, the express-validator is an essential tool. It’s an excellent middleware option that simplifies validating and sanitizing your request data. Explore how to implement an express validator within your project.

First off, you’d install it in your project like so:

bun add express-validator
bun add -d @types/express-validator

Then, in your route postRoute.ts file, you'll need to import and use it like this:

const router = express.Router();

export const postValidator = [
body('title').notEmpty(),
body('content').notEmpty(),
body('author').notEmpty(),
];

router.post('/post',postValidator, createPost);
// other routes

So, with the above code, before we ever hit the route handler /post , the express-validator checks the username, email, and password fields to ensure they meet our criteria. To test the validation, you can make a POST request without passing the required author key or by passing an empty string for one of the fields. This will trigger an error response indicating the missing or empty field.

express-validator

Now that you’ve gained the skills to build an application using Bun, Express, and MongoDB, several additional considerations exist to elevate your project to a production-grade status. To guide you through this advancement, we have curated a collection of tutorials, authoritative documentation, and helpful community contributions.

Wrapping Up

If you’re keen on exploring further, the official documentation for Express.js, MongoDB, and Bun is an excellent starting point.

But, this article should be enough to get you started with building a production ready application using Bun.

I hope you found this article helpful.

Thank you for reading.

--

--

I’m a Full Stack Developer at Anyfin AB who is passionate about new technologies and writing tech blogs. Reach me at tharakaromesh@gmail.com