A production-ready middleware pattern for Express applications.
Install#
npm install @unkey/api expressBasic Middleware#
middleware/auth.ts
import { Unkey } from "@unkey/api";import { Request, Response, NextFunction } from "express";const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! });// Extend Express Request typedeclare global { namespace Express { interface Request { unkey?: V2KeysVerifyKeyResponseData; } }}export async function unkeyAuth( req: Request, res: Response, next: NextFunction,) { // 1. Extract API key const authHeader = req.headers.authorization; if (!authHeader?.startsWith("Bearer ")) { return res.status(401).json({ error: "Missing API key" }); } const apiKey = authHeader.slice(7); let data; try { // 2. Verify with Unkey (throws on transport/SDK errors) ({ data } = await unkey.keys.verifyKey({ key: apiKey })); } catch (err) { console.error("Unkey error:", err); return res .status(503) .json({ error: "Authentication service unavailable" }); } // 3. Check validity if (!data.valid) { const status = data.code === "RATE_LIMITED" ? 429 : 401; return res.status(status).json({ error: data.code }); } // 4. Attach to request and continue req.unkey = data; next();}Use the Middleware#
app.ts
import express from "express";import { unkeyAuth } from "./middleware/auth";const app = express();// Public routesapp.get("/health", (req, res) => { res.json({ status: "ok" });});// Protected routesapp.get("/api/data", unkeyAuth, (req, res) => { // Access verification result const { identity, meta, credits } = req.unkey!; res.json({ message: "Access granted", user: identity?.externalId, plan: meta?.plan, creditsRemaining: credits, });});// Protect entire routerconst apiRouter = express.Router();apiRouter.use(unkeyAuth);apiRouter.get("/users", (req, res) => { res.json({ users: [] });});apiRouter.post("/users", (req, res) => { res.json({ created: true });});app.use("/api/v1", apiRouter);app.listen(3000);With Rate Limit Headers#
Include rate limit info in response headers:
middleware/auth.ts
export async function unkeyAuth( req: Request, res: Response, next: NextFunction,) { const authHeader = req.headers.authorization; if (!authHeader?.startsWith("Bearer ")) { return res.status(401).json({ error: "Missing API key" }); } let data; try { ({ data } = await unkey.keys.verifyKey({ key: authHeader.slice(7) })); } catch { return res.status(503).json({ error: "Service unavailable" }); } // Add rate limit headers if available (v2 uses ratelimits array) if (data.ratelimits?.[0]) { const rl = data.ratelimits[0]; res.set({ "X-RateLimit-Limit": rl.limit.toString(), "X-RateLimit-Remaining": rl.remaining.toString(), "X-RateLimit-Reset": rl.reset.toString(), }); } // Add remaining credits header if available if (data.credits !== undefined) { res.set("X-Credits-Remaining", data.credits.toString()); } if (!data.valid) { const status = data.code === "RATE_LIMITED" ? 429 : 401; return res.status(status).json({ error: data.code }); } req.unkey = data; next();}Permission-Based Access#
Create middleware that requires specific permissions:
middleware/permissions.ts
import { Unkey } from "@unkey/api";import { Request, Response, NextFunction } from "express";const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! });export function requirePermission(permission: string) { return async (req: Request, res: Response, next: NextFunction) => { const authHeader = req.headers.authorization; if (!authHeader?.startsWith("Bearer ")) { return res.status(401).json({ error: "Missing API key" }); } let data; try { ({ data } = await unkey.keys.verifyKey({ key: authHeader.slice(7), permissions: permission, // Check for this permission })); } catch { return res.status(503).json({ error: "Service unavailable" }); } if (!data.valid) { if (data.code === "INSUFFICIENT_PERMISSIONS") { return res.status(403).json({ error: "Forbidden", required: permission, }); } return res.status(401).json({ error: data.code }); } req.unkey = data; next(); };}Use it:
// Anyone with a valid keyapp.get("/api/data", unkeyAuth, handler);// Only keys with "admin" permissionapp.delete("/api/users/:id", requirePermission("admin"), deleteUser);// Only keys with "billing.read" permissionapp.get("/api/invoices", requirePermission("billing.read"), getInvoices);Configurable Middleware Factory#
Create a flexible middleware that can be configured per-route:
middleware/auth.ts
import { Unkey } from "@unkey/api";import { Request, Response, NextFunction } from "express";const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! });interface AuthOptions { permissions?: string; getKey?: (req: Request) => string | null; onError?: (req: Request, res: Response, error: Error) => void; onInvalid?: ( req: Request, res: Response, result: V2KeysVerifyKeyResponseData, ) => void;}export function createAuthMiddleware(options: AuthOptions = {}) { const { permissions, getKey = (req) => req.headers.authorization?.slice(7) ?? null, onError = (req, res) => res.status(503).json({ error: "Service unavailable" }), onInvalid = (req, res, result) => res.status(401).json({ error: result.code }), } = options; return async (req: Request, res: Response, next: NextFunction) => { const apiKey = getKey(req); if (!apiKey) { return res.status(401).json({ error: "Missing API key" }); } let data; try { ({ data } = await unkey.keys.verifyKey({ key: apiKey, permissions, })); } catch (err) { return onError(req, res, err as Error); } if (!data.valid) { return onInvalid(req, res, data); } req.unkey = data; next(); };}// Pre-configured middlewaresexport const unkeyAuth = createAuthMiddleware();export const adminAuth = createAuthMiddleware({ permissions: "admin", onInvalid: (req, res, result) => { if (result.code === "INSUFFICIENT_PERMISSIONS") { return res.status(403).json({ error: "Admin access required" }); } return res.status(401).json({ error: result.code }); },});Error Handling#
Graceful degradation when Unkey is unavailable:
middleware/auth.ts
export async function unkeyAuth( req: Request, res: Response, next: NextFunction,) { const apiKey = req.headers.authorization?.slice(7); if (!apiKey) { return res.status(401).json({ error: "Missing API key" }); } try { const { data } = await unkey.keys.verifyKey({ key: apiKey, }); if (!data.valid) { return res.status(401).json({ error: data.code }); } req.unkey = data; next(); } catch (e) { // Log for monitoring console.error("Unkey verification error:", e); // Option 1: Fail closed (more secure) return res.status(503).json({ error: "Authentication unavailable" }); // Option 2: Fail open (better availability, less secure) // console.warn("Allowing request due to Unkey unavailability"); // return next(); }}TypeScript Setup#
types/express.d.ts
import { V2KeysVerifyKeyResponseData } from "@unkey/api/models/components";declare global { namespace Express { interface Request { unkey?: V2KeysVerifyKeyResponseData; } }}export {};Make sure to include this in your tsconfig.json:
{ "compilerOptions": { "typeRoots": ["./node_modules/@types", "./types"] }}