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