DEV Community

Cover image for Building a REST API with Deno and Hono.js: A Step-by-Step Guide
Francisco Mendes
Francisco Mendes

Posted on

Building a REST API with Deno and Hono.js: A Step-by-Step Guide

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 - web framework
  • denodb - Object-relational Mapper (ORM)
  • 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:

deno init .
Enter fullscreen mode Exit fullscreen mode

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:

{
  "tasks": {
    "dev": "deno run --watch main.ts",
    "build": "deno compile main.ts"
  }
}
Enter fullscreen mode Exit fullscreen mode

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

{
  // ...
  "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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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.

// @/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(),
});
Enter fullscreen mode Exit fullscreen mode

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.

// @/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]);
Enter fullscreen mode Exit fullscreen mode

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.

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

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

const book = new Hono();

// routes come here...

export { book };
Enter fullscreen mode Exit fullscreen mode

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

book.get("/book", async (c) => {
  const list = await Book.all();
  return c.json({ list }, 200);
});
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

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);
});
Enter fullscreen mode Exit fullscreen mode

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.

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);
});
Enter fullscreen mode Exit fullscreen mode

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.

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

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:

// @/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);
Enter fullscreen mode Exit fullscreen mode

To start the process just run this command:

deno task dev
Enter fullscreen mode Exit fullscreen mode

To build the project just run this command:

deno task build
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)