Hono by example

⇐ back to posts

Integrating Clerk authentication with your Hono API

In this guide, we'll walk you through protecting your endpoints using Clerk for authentication, and leveraging Neon and the Drizzle ORM for data storage. Finally, we will deploy the entire solution on Cloudflare Workers.

Creating our project

First, we bootstrap our project. We'll be using pnpm to manage our dependencies. We'll install the Hono framework and the Hono Clerk Middleware.

pnpm create hono@latest

This will start a wizard where we will select Cloudflare Workers as our runtime. Next we install the dependencies that we need:

pnpm install @neondatabase/serverless drizzle-orm @hono/clerk-auth @clerk/backend

We'll also need a few dev dependencies to run our database migrations.

pnpm i -D drizzle-kit dotenv

Next make sure that you've signed up for an account on both Neon and Clerk. On Neon, create a new project and database. You can either do this through the UI or use the CLI, neonctl:

neonctl create project

Make sure to add your DATABASE_URL with the connection string to your .dev.vars file.

Similarily, sign up for Clerk where you will create a new project.

CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=

Database schema definition

Now let's define our database schema definition. Within your src folder create a db subfolder and a schema.ts file. Add the following code to this file, which will be our schema definition.

import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";

export const posts = pgTable("posts", {
  id: serial("id").primaryKey().notNull(),
  user_id: text("user_id").notNull(),
  created_at: timestamp("created_at").defaultNow().notNull(),
  post: text("post").notNull(),
});

Our schema describes a simple use-created message and has the following properties:

  • a primary key that auto increments
  • a user_id field that will store our user that we get back from Clerk
  • a timestamp for when the post was created that defaults to the current date and time.
  • and the contents of the post itself.

We're now ready to create our database migrations and apply the schema to our Neon database.

Generating database migrations

In the root of our project, create a file called drizzle.config.ts and add the following code.

import type { Config } from "drizzle-kit";
import * as dotenv from "dotenv";

dotenv.config({ path: ".env.local" });

if (!process.env.DATABASE_URL)
  throw new Error("DATABASE_URL not found in environment");

export default {
  schema: "./src/db/schema.ts",
  out: "./drizzle",
  dialect: "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_URL,
  },
} satisfies Config;

Here we define where our schema resides and what output folder (./drizzle) to use to place the migrations.

We're now ready to apply the migrations to our Neon database.

run drizzle-kit generate and migrate/push

check if our schema exists!

Creating our API using Hono and protecting our endpoints

import { Hono } from "hono";
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
import { clerkMiddleware, getAuth } from "@hono/clerk-auth";

import { posts } from "./db/schema";

const app = new Hono<{ Bindings: Bindings }>();

app.use("*", clerkMiddleware());

app.get("/foo", (c) => {
  const auth = getAuth(c);
  return c.text("Hello Hono!");
});

app.post("/posts", async (c) => {
  const auth = getAuth(c);

  console.log(auth);

  if (!auth?.userId) {
    return c.json({
      message: "You are not logged in.",
    });
  }

  const param = await c.req.json();

  const sql = neon(c.env.DATABASE_URL);
  const db = drizzle(sql);

  const result = await db
    .insert(posts)
    .values({ user_id: auth.userId, post: param.post })
    .returning({ id: posts.id });
  return c.json({ result });
});

app.get("/posts", async (c) => {
  const auth = getAuth(c);

  clerkClient.user.

  if (!auth?.userId) {
    return c.json({
      message: "You are not logged in.",
    });
  }
  const sql = neon(c.env.DATABASE_URL);
  const db = drizzle(sql);
  const result = await db.select().from(posts);
  return c.json({ result });
});

app.get("/protected", async (c) => {
  const auth = getAuth(c);
  console.log(auth);

  if (!auth?.userId) {
    return c.json({
      message: "You are not logged in.",
    });
  }

  const clerkClient = c.get("clerk");

  try {
    const user = await clerkClient.users.getUser(auth.userId);

    return c.json({
      message: "you are logged in!",
      userId: auth.userId,
      user: user,
    });
  } catch (e) {
    return c.json(
      {
        message: "User not found.",
      },
      404
    );
  }
});

export default app;

Let's define our endpoints and

Testing our API

Before we are ready to test our API we need to get an auth token from Clerk

Run through the instructions outlined here