Create a Backend with Bun, Hono and DrizzleORM

Table of contents

Introduction

Lets check why I use Bun:

Now lets check the framework Hono:

And finally why use DrizzleORM compared to Sequelize or TypeORM:

Its the stack I prefer to use for creating Node projects now, lets see how can we start creating them step by step!

Setup

npm install -g bun
mkdir my-hono-project
cd my-hono-project
bun init

Awesome! Now we created a project with index.ts, package.json, README.md and tsconfig.json!

bun install hono
import { Hono } from "hono";

const app = new Hono();

app.get("/", (c) => {
  return new Response("Hello from Hono and Bun!");
});

This route handles GET requests to the root path (/) and returns a simple response.

You can add more routes for different HTTP methods (GET, POST, PUT, etc.) and define their logic within the callback function.

There we go! An easy way to create a Typescript Node App without installing several packages like in Express! Now lets do some fun stuff.

Setting DrizzleORM

bun install drizzle-orm
bun install drizzle
bun install drizzle-kit -D
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  schema: "./db/schema.ts",
  out: "./db/migrations",
  dialect: "sqlite",
  dbCredentials: {
    url: "file:./sqlite.db",
  },
  verbose: true,
  strict: true,
});
import { drizzle } from "drizzle-orm/bun-sqlite";
import { migrate as migrator } from "drizzle-orm/bun-sqlite/migrator";
import { Database } from "bun:sqlite";
import { join } from "node:path";
import * as schema from "./schema";

const sqlite = new Database("sqlite.db");
export const db = drizzle(sqlite, { schema, logger: true });

export function migrate() {
  migrator(db, { migrationsFolder: join(import.meta.dirname, "migrations") });
}
import { sqliteTable, text } from "drizzle-orm/sqlite-core";

export const users = sqliteTable("users", {
  id: text("id")
    .primaryKey()
    .$defaultFn(() => cuid()),
  username: text("username").notNull().unique(),
  password: text("password").notNull(),
});

export type Users = InferSelectModel<typeof users>;

Routes

At the most basic level we have created a table of users with id, username and password, then using InferSelectModel we get its type to use elsewhere.

bun add @hono/zod-validator
bun install zod

Then we import the following into the file and create the router:

import { zValidator } from "@hono/zod-validator";
import { Hono } from "hono";
import { z } from "zod";
import { db } from "../db";
import { users } from "../db/schema";

const router = new Hono();

Zod

Next we create the type validation using zod:

const credentialsSchema = z.object({
  username: z.string().trim().toLowerCase().email(),
  password: z.string().min(8).max(20),
});

We haven’t used Zod yet in this blog, lets see step by step what all this means:

1. Zod for Validation:

2. credentialsSchema Definition:

3. Validating Username:

4. Validating Password:

router.post("/signup", zValidator("json", credentialsSchema), async (c) => {
  const { username, password } = c.req.valid("json");
  const hashedPassword = await Bun.password.hash(password, "argon2d");

  try {
    await db.insert(users).values({ username, password: hashedPassword });
    return c.body(null, 201);
  } catch (err) {
    console.error(err);
    return c.body(null, 500);
  }
});

Endpoint explanation

Lets explore this EP step by step:

1. Route Definition:

2. Zod Validation Middleware (zValidator):

3. Route Handler Function:

4. Accessing Validated Data:

5. Password Hashing:

6. Database Interaction (Assuming db is a database connection):

7. Response:

import { zValidator } from "@hono/zod-validator";
import { Hono } from "hono";
import { z } from "zod";
import { db } from "../db";
import { users } from "../db/schema";

const router = new Hono();

const credentialsSchema = z.object({
  username: z.string().trim().toLowerCase().email(),
  password: z.string().min(8).max(20),
});

router.post("/signup", zValidator("json", credentialsSchema), async (c) => {
  const { username, password } = c.req.valid("json");
  const hashedPassword = await Bun.password.hash(password, "argon2d");

  try {
    await db.insert(users).values({ username, password: hashedPassword });
    return c.body(null, 201);
  } catch (err) {
    console.error(err);
    return c.body(null, 500);
  }
});

export default router;
import { Hono } from "hono";
import usersRoute from "./routes/users";
import { migrate } from "./db";

// Apply migrations
migrate();

const app = new Hono();

app.route("/", usersRoute);

export default app;

And we have our first endpoint! But we need to work on the migrations now

Migrations

Migrations are a version-controlled approach to updating your database schema. They involve writing scripts (migration files) that define changes like adding tables, modifying columns, or adding constraints. A migration runner tool executes these scripts in order, ensuring all environments have the same up-to-date database structure, simplifying deployments, and allowing you to roll back changes if necessary.

Lets create our DB first inside our rootfile sqlite.db, it will be empty for now.

Next on the terminal we run the command bun drizzle-kit generate, which automatically creates migration files based on your DrizzleORM schema definitions. This saves you time and reduces errors compared to manual script writing, ensuring your database schema stays in sync with your application’s evolving needs.

And finally run bun --watch index.ts and we have our app running!

Conclusion

This is the first step into creating a backend application using Bun, Hono and DrizzleORM, its very quick to setup and performance wise its very fast. For our next post we go more into creating more endpoints.

See you on the next post.

Sincerely,

Eng. Adrian Beria.