Skip to main content
A production-ready middleware pattern for Express applications.

Install

npm install @unkey/api express

Basic 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 type
declare 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);

  // 2. Verify with Unkey
  const { meta, data, error } = await unkey.keys.verifyKey({
    key: apiKey,
  });

  // 3. Handle errors
  if (error) {
    console.error("Unkey error:", error);
    return res.status(503).json({ error: "Authentication service unavailable" });
  }

  // 4. Check validity
  if (!data.valid) {
    const status = data.code === "RATE_LIMITED" ? 429 : 401;
    return res.status(status).json({ error: data.code });
  }

  // 5. 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 routes
app.get("/health", (req, res) => {
  res.json({ status: "ok" });
});

// Protected routes
app.get("/api/data", unkeyAuth, (req, res) => {
  // Access verification result
  const { identity, meta, remaining } = req.unkey!;
  
  res.json({
    message: "Access granted",
    user: identity?.externalId,
    plan: meta?.plan,
    creditsRemaining: remaining,
  });
});

// Protect entire router
const 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" });
  }

  const { meta, data, error } = await unkey.keys.verifyKey({
    key: authHeader.slice(7),
  });

  if (error) {
    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" });
    }

    const { meta, data, error } = await unkey.keys.verifyKey({
      key: authHeader.slice(7),
      permissions: permission,  // Check for this permission
    });

    if (error) {
      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 key
app.get("/api/data", unkeyAuth, handler);

// Only keys with "admin" permission
app.delete("/api/users/:id", requirePermission("admin"), deleteUser);

// Only keys with "billing.read" permission
app.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" });
    }

    const { meta, data, error } = await unkey.keys.verifyKey({
      key: apiKey,
      permissions,
    });

    if (error) {
      return onError(req, res, error);
    }

    if (!data.valid) {
      return onInvalid(req, res, data);
    }

    req.unkey = data;
    next();
  };
}

// Pre-configured middlewares
export 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 { meta, data, error } = await unkey.keys.verifyKey({
      key: apiKey,
    });

    if (error) {
      // Log for monitoring
      console.error("Unkey verification error:", error);
      
      // 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();
    }

    if (!data.valid) {
      return res.status(401).json({ error: data.code });
    }

    req.unkey = data;
    next();
    
  } catch (e) {
    console.error("Unexpected auth error:", e);
    return res.status(503).json({ error: "Authentication service error" });
  }
}

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"]
  }
}
Last modified on February 6, 2026