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:
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 hono.dev 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.
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.
middleware.ts
import { verifyKey } from"@unkey/api";importtype { Context, Next } from"hono";importtype { Variables } from"./index";exportasyncfunctionmiddleware(c:Context<{ Variables:Variables }, "/api/v1/*">,next:Next,) {constkey= c.req.header("x-openstatus-key");if (!key) return c.text("Unauthorized", 401);const { error, result } =awaitverifyKey(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);awaitnext();}
middleware.ts
import { verifyKey } from"@unkey/api";importtype { Context, Next } from"hono";importtype { Variables } from"./index";exportasyncfunctionmiddleware(c:Context<{ Variables:Variables }, "/api/v1/*">,next:Next,) {constkey= c.req.header("x-openstatus-key");if (!key) return c.text("Unauthorized", 401);const { error, result } =awaitverifyKey(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);awaitnext();}
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.
monitor.ts
importtype { Variables } from"./index";exportconstmonitorApi=newHono<{ Variables:Variables }>();monitorApi.get("/:id", async (c) => {constworkspaceId= c.get("workspaceId");const { id } = c.req.valid("param");// ...fetch data from your database [e.g. via Drizzle ORM]constmonitor=await db .select() .from(monitor) .where(and(eq(monitor.id, Number(id)),eq(monitor.workspaceId, Number(workspaceId)), ), ) .get();return c.json(monitor);});
monitor.ts
importtype { Variables } from"./index";exportconstmonitorApi=newHono<{ Variables:Variables }>();monitorApi.get("/:id", async (c) => {constworkspaceId= c.get("workspaceId");const { id } = c.req.valid("param");// ...fetch data from your database [e.g. via Drizzle ORM]constmonitor=await db .select() .from(monitor) .where(and(eq(monitor.id, Number(id)),eq(monitor.workspaceId, Number(workspaceId)), ), ) .get();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:
For OpenStatus, we are running our Hono.js server on fly.io
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.
Conclusion
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.