Initial Hono Server Setup
If you've worked on a Node.js project in the past, you might have worked with Express to set up a web server or to create a REST API. Let's take a look at how we might do something similar with Hono. It's a small, simple framework that we can use with any runtime, but of course we're going to use it with Deno.
All right, so the first thing that we want to do here is we're going to add Hono to our project with JSR. So we'll say deno add jsr:@hono/hono.
Once this has been added, we can start in our main file to create the basic Hono setup. So we'll import it from @hono/hono. We then will create an app which is a new instance of Hono, and then we'll create a small route. We'll say app.get when we go to the / route, we want to take the context, and we'll say return c.text('Hello from the trees').
So we're going to be making a tree API in a minute. Then we'll call Deno.serve, pass in app.fetch, and then if we run this, we'll use the -A flag here, we'll say main.ts. So if we go over here to localhost:8000, we should see this running in the browser.
Defining a Data Model and Storage Helpers
All right, so now that we've set up this simple server, we can start to build out our quick database. Keep in mind that you can use any persistent data storage with Deno—Postgres, SQL, whatever you like, you can use it. But for now, let's start to create a little container for the data that we're going to use. We're going to create an interface called Tree and it should have a few fields on it like an ID, a species, an age, a location. And then we'll create some data.
So we'll say const Oak. This is a Tree, of course. And then we'll say ID is three, species is Oak. We'll have its age and the location.
All right, so from here what we can do is create a few different helper functions that are going to help us interact with local storage. So what we'll do here is we'll create a function called setItem. This is going to take in a key which is a string and a value which is of type Tree. And then this should set in local storage using the setItem function the key, and then we'll JSON.stringify whatever the value is. So this is going to handle the types, but it's also going to make sure that the right fields and values are sent in.
So next up, we will say const getItem and we'll say key is a string. Tree or null. And then we'll say that the item is whatever localStorage.getItem returns with that key. And then we'll say return if there's an item, we want to parse it as JSON, otherwise we want to return null.
Let's just give it a little test here. We'll say setItem—and remember, it's taking in the key of what we want to set, and then the value, which is whatever the tree is. So we'll say trees_oak. And then the other value will be Oak. Then we'll say const newTree = getItem('trees_oak'), and then we'll console log that new tree. Okay, so we're getting the item, we're setting the item. So let's go ahead and stop the app. We'll run it again, deno run -A main.ts. The first thing it's going to do is to console log that new tree that's been added. So far so good.
Testing Local Storage Updates
We can also use setItem to update the record, so that if the key already exists, then the value is going to be updated. So let's do something like this. Maybe we'll change the age to four. We'll give that a save. We'll stop and restart, and now our item has been set with a new age. Okay, so those are the bones of what we're going to do, but now it's time to use Hono routing to create some REST API routes now that we understand how to work with these methods.
Implementing the CREATE Route
So what we'll do beneath here is we're going to say app.post. So anytime we post to the /trees route, we want to call this function. It's going to be an async function that takes in that context. We're going to say treeDetails = await c.req.json(). Then we're going to say tree, which is of type Tree, should be equal to those treeDetails. Then we'll call setItem.
So here we'll say trees_${tree.id}, comma tree. And then we'll say return c.json and then we'll add a message here: We just added a ${tree.species} tree.
Okay, so now that we have that in place, we're going to send a cURL request. You can type this from scratch, you could use Postman or your favorite API testing tool. I will just type it all out. And then it will say headers Content-Type: application/json. The data should be: ID 2, a species is a Willow tree, and the age is 100, location is Juniper Park. And then we want to make sure that that whole thing is wrapped in single quotes. And then the message that we get back is 'We just added a Willow tree'.
Implementing the READ Route
To prove that we created that tree, let's get the data by its ID. So we're going to do this. We'll say app.get('/trees/:id'). This is another async function that we're going to use to grab the ID. So we'll say const id = c.req.param('id'). Then we'll say const tree = getItem('trees_' + id). And then we'll say if no tree is found, 'Tree not found', and then we'll add a 404 there. Outside of that set of curly braces, we'll return the data if it exists. Now, if we make sure that this thing is running with those changes, we're going to go to our /trees/3 and we should see that is there. /trees/2 has our latest one. All right.
Implementing the UPDATE Route
So we also want to be able to handle these updates, which we should do with a put request. We'll say /trees/:id, async. const id = await c.req.param('id'). Then let's destructure the species, age, and location from that response. So we'll say await c.req.json(). We should add that await there too. Next, let's go ahead and say updatedTree is of type Tree: ID, species, age, location. That's just another way to do that. You could have done that up here on line 38 as well.
All right, so now that we have that, we can set the item: trees_${id} and we'll pass along updatedTree. And now we'll say return c.json({ message: 'Tree has changed', ... }). And then if you wanted to say location, species, age, something like that, you could.
All right, so let's go ahead and add that cURL request. This time it's going to be a put request to a particular route, a particular endpoint. So we'll change some data about this number two here. So we'll say put and the ID will stay the same. We'll say the age is 301. Happy birthday to that tree. And then the tree has been moved to Legacy Park. All right, so now if we hit enter, all of these details have changed. If we go to the browser, we can see the most recent details as well.
Implementing the DELETE Route
Finally, this is a CRUD application. We need the delete here. So we'll say app.delete('/trees/:id'), async. Now this time we're going to delete based on that ID, so we need to grab it from the request parameters. So we also need a way of interacting with the local storage item. So we'll say deleteItem, key is a string. localStorage.removeItem(key). And now we can use that here: deleteItem('trees_' + id). Return c.json({ message: Tree ${id} has been cut down }).
That is the saddest thing ever. But let's run it again. And we don't even need this part, right? No more data. It knows because of this 2. So let's hit enter. 'Tree 2 has been cut down.' Refresh. 'Tree not found'.
So we've used Deno in combination with Hono to build a little REST API for our tree data.
Deployment Options and Conclusion
If we wanted to deploy this, we could deploy with zero configuration to Deno Deploy. Everything would just work right away. Or we could deploy it using the official Docker image over here at deno.land/docker. You could deploy to AWS, Google Cloud Platform, Digital Ocean, or whatever your favorite cloud provider is.