Building a REST API with Deno and Hono.js: A Step-by-Step Guide
Tags: deno, tutorial, javascript, webdev
Published: 2023-08-13 21:40:08

In the last few months I've been using Deno almost daily and I have been thoroughly impressed with this JavaScript runtime. For this very reason, I decided to share with you the process of creating a REST API. By the end of this article, we'll compile the project into a self-contained executable with great portability.

# Introduction

In this article we are going to create a REST API in which we perform the famous CRUD and so that everyone has the possibility to test locally, the database that will be used is SQLite.

To give you a little more context, in this article we are going to use the following technologies:
- [hono](https://github.com/honojs/hono) - web framework
- [denodb ](https://github.com/eveningkid/denodb) - Object-relational Mapper (ORM)
- [zod](https://github.com/colinhacks/zod) - schema validation

Before starting this article, I recommend that you have Deno installed and that you have a brief experience using Node.

# Set up project

To get started, navigate to the directory of your choice and run the following command:

```sh
deno init .
```

The above command is expected to have created a set of files in the workspace, with this we have initialized a Deno project and are going to make some changes to the `deno.jsonc` file.

Starting by defining some of the commands that we are going to run with task runner `deno task`:

```js
{
  "tasks": {
    "dev": "deno run --watch main.ts",
    "build": "deno compile main.ts"
  }
}
```

Next, let's define some dependencies that need to be imported into the project:

```js
{
  // ...
  "imports": {
    "hono": "https://deno.land/x/hono@v3.2.6/mod.ts",
    "hono/middleware": "https://deno.land/x/hono@v3.2.6/middleware.ts",
    "server": "https://deno.land/std@0.192.0/http/server.ts",
    "denodb": "https://deno.land/x/denodb@v1.0.40/mod.ts",
    "zod": "https://deno.land/x/zod@v3.21.4/mod.ts"
  }
}
```

With this last change in `deno.jsonc` we can give the project configuration as finished, however if you are interested in learning more about the subject and extending the project configuration, you can take a look [here](https://deno.land/manual@v1.36.1/getting_started/configuration_file).

But now is the time to get your hands dirty!

# Create database schema and client

The next step will be to create the entities present in the project, in this case we will have only one, which will be the `Book`, but in addition to the entity we will also need a schema validation.

```ts
// @/db/models/book.ts
import { DataTypes, Model } from "denodb";
import { z } from "zod";

export class Book extends Model {
  static table = "books";
  static timestamps = true;

  static fields = {
    id: {
      type: DataTypes.INTEGER,
      primaryKey: true,
      autoIncrement: true,
    },
    title: {
      type: DataTypes.STRING,
      allowNull: false,
      length: 25,
    },
    description: {
      type: DataTypes.STRING,
      allowNull: false,
      length: 100,
    },
    isAvailable: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
    },
  };

  static defaults = {
    isAvailable: true,
  };
}

export const bookSchema = z.object({
  title: z.string(),
  description: z.string(),
  isAvailable: z.boolean(),
});
```

With the entity created, now we need to create the connection to the database and later synchronize it. To do so, first we will need to import the `Book` entity that we have just created, then we will create the connector and the client instance of the database and from this, we will sync with the database.

```ts
// @/db/connect.ts
import { Database, SQLite3Connector } from "denodb";

import { Book } from "./models/book.ts";

const connector = new SQLite3Connector({
  filepath: "./dev.sqlite",
});

export const db = new Database(connector);

db.link([Book]);
```
In this way, we have already created the schema and the client of the database and we can move on to the next step.

# Define the routes

Taking into account what was created in the last points, we can now move on to defining the API routes. First we need to import Hono, in order to create a router, and we need to import the `Book` entity and the `bookSchema` schema.

```ts
// @/router/book.ts
import { Hono } from "hono";

import { Book, bookSchema } from "../db/models/book.ts";

const book = new Hono();

// routes come here...

export { book };
```

With this done, we can now define the first route, in which we will get all the books stored in the database.

```ts
book.get("/book", async (c) => {
  const list = await Book.all();
  return c.json({ list }, 200);
});
```

In the next route, in the endpoint we will define the `id` query param to obtain a specific book taking into account its unique identifier.

```ts
book.get("/book/:id", async (c) => {
  const { id } = c.req.param();
  const book = await Book.where("id", id).first();
  return c.json(book, 200);
});
```

Currently we have two routes defined, one to get all books and another to get a specific book. But currently the database is empty and we need to create a route responsible for inserting a new book into the database. 

With this route, you have to define the json with the book's data in the body of the request so that it can be inserted and to guarantee that we are inserting the expected data, we are going to use `bookSchema`.

```ts
book.post("/book", async (c) => {
  const body = await c.req.json();

  const val = bookSchema.safeParse(body);
  if (!val.success) return c.text("Invalid!", 500);

  await Book.create({ ...val.data });
  return c.body("Created", 201);
});
```

After that, since we managed to create a new book, we also have to be able to update it. For this we will need to define the `id` query param in the route, so that we know which book we want to update, then in the body of the request it is expected that the data will be as expected.

```ts
book.put("/book/:id", async (c) => {
  const { id } = c.req.param();
  const body = await c.req.json();

  const val = bookSchema.safeParse(body);
  if (!val.success) return c.text("Invalid!", 500);

  await Book.where("id", id).update({ ...val.data });
  return c.body("Updated", 200);
});
```

Last but not least, it remains to implement the route responsible for deleting a book taking into account the value of the `id` query param.

```ts
book.delete("/book/:id", async (c) => {
  const { id } = c.req.param();
  await Book.deleteById(id);
  return c.body("Deleted", 200);
});
```

Now we can say that we have each of the routes registered, which allows us to go to the next and last step.

# Set up middlewares

In this step, we are going to import the necessary middlewares for the application, not forgetting the router that was created just now, as well as initialize the database synchronization and serve the api. Like this:

```ts
// @/main.ts
import { Hono } from "hono";
import { cors, logger, prettyJSON } from "hono/middleware";
import { serve } from "server";

import { book } from "./router/book.ts";
import { db } from "./db/connect.ts";

const api = new Hono();

api.use("*", logger());
api.use("*", prettyJSON());
api.use("/api/*", cors());

api.route("/api", book);
api.notFound((c) => c.json({ message: "Not Found" }, 404));

await db.sync();
serve(api.fetch);
```

To start the process just run this command:

```sh
deno task dev
```

To build the project just run this command:

```sh
deno task build
```

# Conclusion

I hope you found this article helpful, whether you're using the information in an existing project or just giving it a try for fun.

Please let me know if you notice any mistakes in the article by leaving a comment. And, if you'd like to see the source code for this article, you can find it on the github repository linked below.

[Github Repo](https://github.com/FranciscoMendes10866/deno-hono-api)