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:
- Bun (follow the installation instructions on their website)
- 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.
- Log in to your MongoDB Atlas account and create a new cluster (or use an existing one).
- In the cluster view, click on the "Connect" button.
- Choose "Connect your application" and select "Node.js" as your driver.
- 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:
It imports necessary modules: Hono for the web framework, serve for running the server, env for accessing environment variables, and MongoClient for MongoDB connection.
A Hono app instance is created, and a port is set.
The
connectToMongo
function connects to MongoDB Atlas using the provided URI.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.
- Retrieves the
A basic route ('/') is defined, responding with "Hello Hono with Bun!".
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:
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
Get all items:
curl http://localhost:3000/items
Get a single item (replace
<id>
with an actual item ID):curl http://localhost:3000/items/<id>
Update an item:
curl -X PUT -H "Content-Type: application/json" -d '{"name":"Updated Item"}' http://localhost:3000/items/<id>
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:
- Add authentication and authorization to secure your endpoints.
- Implement pagination for the "Get all items" endpoint to handle large datasets.
- Add more complex validation and error handling (try out something like Zod).
- 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!