How to secure your API with Unkey and Hono.js Middleware

How to secure your API with Unkey and Hono.js Middleware
Maximilian Kaske

Oct 01, 20235 min read


Why do we need to secure our APIs? Well, there are many reasons for that. The most important one is that we want to protect your data from unauthorized access.

We will learn how to secure our Hono.js API server with Unkey.

Unkey is a service that allows you to create and manage API keys. It includes useful features like:

  • Rate limiting: avoid getting DDoSed
  • Temporary keys: when working on free trials
  • Key limitation: limit the number of max. requests

The best part: We have no additional database migration or setup to do. Just use the Unkey API and/or SDK to create, revoke and verify keys.

Hono is similar to express just hipper and runs on the edge.

How does OpenStatus use Unkey?

Whenever OpenStatus creates an API key, we will send a request to the Unkey API using their Typescript SDK with the a specific ownerId which will be the workspaceId in our case. The user will get an API key back which they can use to access their content via our API route. Unkey will match the API key to the ownerId and we will be able to validate that the request is the owner of the workspaceId.

As an example, here the Next.js server action (see GitHub) to allow users to create and revoke their own API keys:

"use server";
import { Unkey } from "@unkey/api";
const unkey = new Unkey({ token: process.env.UNKEY_TOKEN });
export async function create(ownerId: number) {
  const key = await unkey.keys.create({
    apiId: process.env.UNKEY_API_ID,
    ownerId: String(ownerId), // workspaceId
    prefix: "os", // os_1234567890
    // include more options like 'ratelimit', 'expires', 'remaining'
  return key;
export async function revoke(keyId: string) {
  const res = await unkey.keys.revoke({ keyId });
  return res;

To test key creation, you can simply go to the Unkey Dashboard and create an API key manually instead of using the SDK. The SDK is useful once you want your users to create API keys programmatically.

Getting started

Checkout if you want to set up a new project or follow along if you already have a Hono.js project.

We will pinpoint the most important parts of the setup. You can find the full code source on GitHub.

Create the base path

That's as simple as it looks. Create a new Hono() instance and define the routes (route) and middlewares (use).

For the sake of this example, we only consider the /api/v1/monitor route.

import { middleware } from "./middleware";
import { monitorApi } from "./monitor";
export type Variables = { workspaceId: string }; // Context
const api = new Hono<{ Variables: Variables }>().basePath("/api/v1");
api.use("/*", middleware);
api.route("/monitor", monitorApi);
export default app;

Create the middleware

The middleware will automatically be applied to all routes that match the path /api/v1/*. We will use the x-openstatus-key request header to append the API key and verify it on our server.

The Hono Context will be used to store the workspaceId we are retrieving from Unkey and sharing it across the application.

Here, we are verifying the API key via the @unkey/api package. It returns either an error or the result.valid whether or not to grant access to the user.

import { verifyKey } from "@unkey/api";
import type { Context, Next } from "hono";
import type { Variables } from "./index";
export async function middleware(
  c: Context<{ Variables: Variables }, "/api/v1/*">,
  next: Next,
) {
  const key = c.req.header("x-openstatus-key");
  if (!key) return c.text("Unauthorized", 401);
  const { error, result } = await verifyKey(key);
  // up to you if you want to pass the actual message to your users
  // or simply return "Internal Server Error"
  if (error) return c.text(error.message, 500);
  if (!result.valid) return c.text("Unauthorized", 401);
  c.set("workspaceId", result.ownerId);
  await next();

Create the route

Every route, here monitorApi, will have access to the workspaceId via the Context and therefore can query the database for the workspace.

import type { Variables } from "./index";
export const monitorApi = new Hono<{ Variables: Variables }>();
monitorApi.get("/:id", async (c) => {
  const workspaceId = c.get("workspaceId");
  const { id } = c.req.valid("param");
  // ...fetch data from your database [e.g. via Drizzle ORM]
  const monitor = await db
        eq(, Number(id)),
        eq(monitor.workspaceId, Number(workspaceId)),
  return c.json(monitor);

Read more about the Hono path parameter ":id" in their docs.

Test it

Once your project is running, you can test your implementation with the following curl command to access your monitor with the id 1:

curl --location 'http://localhost:3000/api/v1/monitor/1' \
--header 'x-openstatus-key: os_1234567890'

For OpenStatus, we are running our Hono.js server on with bun via bun run src/index.ts.

We have included the @hono/zod-openapi plugin to generate an OpenAPI spec out of the box. Read more about the supported endpoints in our docs.


Et voilà. We have secured our API with Unkey and the Hono.js middleware, only allowing authorized users to access their data.

Unkey increases our velocity and helps us focus on what's relevant to the user, not the infrastructure behind it. We also get key verification insights out of the box and can target specific users based on their usage.

@chronark_ has recently published an @unkey/hono package that uses a similar implementation under the hood, reducing some boilerplate code for you. Highly recommend checking it out if you are using Hono.js.