CRUD App with Hono, MongoDB, Bun and TypeScript

Hono is designed to create fast, efficient web applications and APIs, particularly suited to edge computing environments. It's been getting a lot of positive attention lately, so I thought we should learn it together.

In this article, we'll work with Hono and Bun to create an API to learn how to build with Hono.

So why Hono and Bun?

Hono is known for its simplicity, high performance, and low overhead, making it one of the fastest JavaScript web frameworks. Bun complements this by providing a fast JavaScript runtime with built-in tooling, making it an excellent choice for developing and running Hono applications.

I also like that Hono is written in TypeScript and provides excellent TypeScript support out of the box. Bun also has great TypeScript support, making this combination ideal for TypeScript developers.

Prerequisites

Before we begin, make sure you have the following installed and set up:

  1. Bun (follow the installation instructions on their website)
  2. A MongoDB Atlas account (free tier is sufficient for this tutorial)

Setting Up the Project

Let's set up our project using Bun:

bun create hono hono-mongodb-crud

Choose bun as the option (it'll look like this):

? Which template do you want to use?
    aws-lambda
❯   bun
    cloudflare-pages
    cloudflare-workers
    deno
    fastly
    nextjs
    nodejs
    vercel

Then move into your project:

cd hono-mongodb-crud

You now have new Hono project with Bun. Now, let's install the MongoDB driver:

bun add mongodb

Connecting to MongoDB Atlas

Before coding, we must set up our MongoDB Atlas cluster and get our connection string.

  1. Log in to your MongoDB Atlas account and create a new cluster (or use an existing one).
  2. In the cluster view, click on the "Connect" button.
  3. Choose "Connect your application" and select "Node.js" as your driver.
  4. Copy the connection string provided. It should look something like this:
mongodb+srv://<username>:<password>@cluster0.xxxxx.mongodb.net/?retryWrites=true&w=majority

Now, let's create a .env file in our project root to store our MongoDB connection string securely:

MONGODB_URI=mongodb+srv://<username>:<password>@cluster0.xxxxx.mongodb.net/?retryWrites=true&w=majority

Replace <username> and <password> with your MongoDB Atlas credentials.

Creating the Hono Application

Now that our project and database connection are ready, let's create our main application file. You should already have an index.ts in the /src folder of your project. Delete the contents and paste the following:

import { Hono } from "hono";
import { MongoClient, Db } from "mongodb";

const app = new Hono();
const port = 3000;

// MongoDB connection
let db: Db;

// Connect to MongoDB
async function connectToMongo(uri: string): Promise<void> {
  try {
    const client = new MongoClient(uri);
    await client.connect();
    db = client.db("crud_demo");
    console.log("Connected to MongoDB Atlas");
  } catch (error) {
    console.error("Error connecting to MongoDB:", error);
    process.exit(1);
  }
}

// Middleware to connect to MongoDB
app.use("*", async (c, next) => {
  const MONGODB_URI = Bun.env.MONGODB_URI;
  if (!MONGODB_URI) {
    throw new Error("MONGODB_URI is not defined in environment variables");
  }
  if (!db) {
    await connectToMongo(MONGODB_URI);
  }
  return next();
});

// Basic route
app.get("/", (c) => c.text("Hello Hono with Bun!"));

// This export controls the server start
export default {
  port,
  fetch: app.fetch,
};

This sets up our basic Hono application and connects it to MongoDB Atlas. If it isn't obvious what's happening by reading the code, here's an explanation:

  1. It imports necessary modules: Hono for the web framework, serve for running the server, env for accessing environment variables, and MongoClient for MongoDB connection.

  2. A Hono app instance is created, and a port is set.

  3. The connectToMongo function connects to MongoDB Atlas using the provided URI.

  4. A middleware is set up using app.use('*', ...) that runs for all routes. It:

    • Retrieves the MONGODB_URI from environment variables using Bun.
    • Checks if a database connection exists, and if not, it connects to MongoDB.
    • This ensures the database is connected before handling any requests.
  5. A basic route ('/') is defined, responding with "Hello Hono with Bun!".

  6. Finally, it logs a message indicating the server is running.

One final note: You might notice the c parameter being passed on to all requests. We use This "context" object to handle requests and responses.

This provides the foundation for developing more complex routes and functionality.

## Start/Test the App

Run the server and test everything is running as expected now by running:

npm run dev

Then go to http://localhost:3000/, and you should see:

Hello Hono with Bun!

Implementing CRUD Operations

Now, let's implement our CRUD operations. We'll create routes for creating, reading, updating, and deleting items. Add the following code to your app.js file:

// Update MongoDB import to include ObjectId
import { MongoClient, Db, ObjectId } from "mongodb"

// Create
app.post('/items', async (c) => {
  try {
    const item = await c.req.json()
    const result = await db.collection('items').insertOne(item)
    return c.json({ id: result.insertedId }, 201)
  } catch (error) {
    return c.json({ error: 'Failed to create item' }, 500)
  }
})

// Read (all)
app.get('/items', async (c) => {
  try {
    const items = await db.collection('items').find().toArray()
    return c.json(items)
  } catch (error) {
    return c.json({ error: 'Failed to retrieve items' }, 500)
  }
})

// Read (single)
app.get('/items/:id', async (c) => {
  try {
    const id = c.req.param('id')
    const item = await db.collection('items').findOne({ _id: new ObjectId(id) })
    if (!item) return c.json({ error: 'Item not found' }, 404)
    return c.json(item)
  } catch (error) {
    return c.json({ error: 'Failed to retrieve item' }, 500)
  }
})

// Update
app.put('/items/:id', async (c) => {
  try {
    const id = c.req.param('id')
    const updates = await c.req.json()
    const result = await db.collection('items').updateOne(
      { _id: new ObjectId(id) },
      { $set: updates }
    )
    if (result.matchedCount === 0) return c.json({ error: 'Item not found' }, 404)
    return c.json({ message: 'Item updated successfully' })
  } catch (error) {
    return c.json({ error: 'Failed to update item' }, 500)
  }
})

// Delete
app.delete('/items/:id', async (c) => {
  try {
    const id = c.req.param('id')
    const result = await db.collection('items').deleteOne({ _id: new ObjectId(id) })
    if (result.deletedCount === 0) return c.json({ error: 'Item not found' }, 404)
    return c.json({ message: 'Item deleted successfully' })
  } catch (error) {
    return c.json({ error: 'Failed to delete item' }, 500)
  }
})

As you can see, this will take in any object and do CRUD operations on it (which probably isn't wise for a public application). Let's update it with some lightweight validation to ensure we get at least a name added in the object:

Custom Middleware

Let's add some basic error handling and validation to make our application more robust. We'll create a simple middleware for validating the item structure:

// import types from Hono
import { Hono, type Context, type Next } from "hono";

// Middleware to ensure we have a name
const validateItem = async (c: Context, next: Next) => {
  const item = await c.req.json()
  if (!item.name || typeof item.name !== 'string') {
    return c.json({ error: 'Invalid item: name is required and must be a string' }, 400)
  }
  // Add more validation as needed
  await next()
}

// Apply middleware to POST and PUT routes
app.post('/items', validateItem, async (c) => {
  // ... (existing code)
})

app.put('/items/:id', validateItem, async (c) => {
  // ... (existing code)
})

Testing the Application

You can use tools like cURL, Postman, or your browser's console to test the endpoints. Here are some example requests:

  1. Create an item:

    curl -X POST -H "Content-Type: application/json" -d '{"name":"Test Item","description":"This is a test item"}' http://localhost:3000/items
    
  2. Get all items:

    curl http://localhost:3000/items
    
  3. Get a single item (replace <id> with an actual item ID):

    curl http://localhost:3000/items/<id>
    
  4. Update an item:

    curl -X PUT -H "Content-Type: application/json" -d '{"name":"Updated Item"}' http://localhost:3000/items/<id>
    
  5. Delete an item:

    curl -X DELETE http://localhost:3000/items/<id>
    

Next Steps

This application provides a solid foundation for building more complex web applications and APIs.

Here are some ideas for further improvement:

  1. Add authentication and authorization to secure your endpoints.
  2. Implement pagination for the "Get all items" endpoint to handle large datasets.
  3. Add more complex validation and error handling (try out something like Zod).
  4. Implement logging for better debugging and monitoring.

You'll never learn new tools properly by following along. Take this and run with it. I can't wait to see what you build.

Happy coding!

TypeScriptTutorialBun
Avatar for Niall Maher

Written by Niall Maher

Founder of Codú - The web developer community! I've worked in nearly every corner of technology businesses: Lead Developer, Software Architect, Product Manager, CTO, and now happily a Founder.

Loading

Fetching comments

Hey! 👋

Got something to say?

or to leave a comment.