Skip to main content

Overview

This guide covers everything you need to protect your Express.js API with Unkey, including basic setup, reusable middleware, permission checks, and production-ready patterns. What you’ll learn:
  • Quick setup with the Unkey SDK
  • Reusable middleware patterns
  • Permission-based access control
  • Rate limiting integration
  • Error handling best practices

Prerequisites

Quick Start

1

Create your Express app

mkdir unkey-express && cd unkey-express
npm init -y
npm install express @unkey/api dotenv
2

Add your root key

Get a root key from Settings → Root Keys and create a .env file:
.env
UNKEY_ROOT_KEY="unkey_..."
Never commit your .env file. Add it to .gitignore.
3

Create your server

Create index.js with a protected route:
index.js
const express = require("express");
const { verifyKey } = require("@unkey/api");
require("dotenv").config();

const app = express();
const port = process.env.PORT || 3000;

// Public route
app.get("/", (req, res) => {
  res.json({ message: "Welcome! Try /secret with an API key." });
});

// Protected route
app.get("/secret", async (req, res) => {
  // 1. Extract the key from the Authorization header
  const authHeader = req.headers.authorization;
  const key = authHeader?.replace("Bearer ", "");

  if (!key) {
    return res.status(401).json({ error: "Missing API key" });
  }

  // 2. Verify with Unkey
  try {
    const { meta, data } = await verifyKey({ key });

    if (!data.valid) {
      return res.status(401).json({ 
        error: "Invalid API key",
        code: data.code 
      });
    }

    // 3. Key is valid — return protected data
    res.json({
      message: "Welcome to the secret route!",
      keyId: data.keyId,
      identity: data.identity,
    });
  } catch (err) {
    console.error(err);
    return res.status(500).json({ error: "Could not verify key" });
  }
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});
4

Start your server

node index.js
5

Test it

First, create a test key in your Unkey dashboard, then:
Test with valid key
curl http://localhost:3000/secret \
  -H "Authorization: Bearer YOUR_API_KEY"
You should see:
{
  "message": "Welcome to the secret route!",
  "keyId": "key_...",
  "identity": null
}

Reusable Middleware

For cleaner code, extract verification into middleware:
middleware/auth.js
const { verifyKey } = require("@unkey/api");

async function unkeyAuth(req, res, next) {
  const key = req.headers.authorization?.replace("Bearer ", "");

  if (!key) {
    return res.status(401).json({ error: "Missing API key" });
  }

  try {
    const { meta, data } = await verifyKey({ key });

    if (!data.valid) {
      return res.status(401).json({ error: "Invalid API key" });
    }

    // Attach key info to request for use in route handlers
    req.unkey = data;
    next();
  } catch (err) {
    console.error(err);
    return res.status(500).json({ error: "Could not verify key" });
  }
}

module.exports = { unkeyAuth };
Use it on any route:
const { unkeyAuth } = require("./middleware/auth");

app.get("/secret", unkeyAuth, (req, res) => {
  // req.unkey contains the verification result
  res.json({ message: "Secret data", keyId: req.unkey.keyId });
});

app.get("/another-secret", unkeyAuth, (req, res) => {
  res.json({ data: "More protected content" });
});

TypeScript Version

For TypeScript projects:
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
) {
  const authHeader = req.headers.authorization;
  
  if (!authHeader?.startsWith("Bearer ")) {
    return res.status(401).json({ error: "Missing API key" });
  }
  
  const apiKey = authHeader.slice(7);

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

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

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

  req.unkey = data;
  next();
}

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
  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,
    });

    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:
import { unkeyAuth } from "./middleware/auth";
import { requirePermission } from "./middleware/permissions";

// 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);

Protect Entire Router

Apply middleware to an entire router:
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 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);

Verification Response Data

After successful verification, data contains:
FieldTypeDescription
validbooleanWhether the key passed all checks
codestringStatus code (VALID, NOT_FOUND, RATE_LIMITED, etc.)
keyIdstringThe key’s unique identifier
namestring?Human-readable name of the key
metaobject?Custom metadata associated with the key
expiresnumber?Unix timestamp (in milliseconds) when the key will expire
creditsnumber?Remaining uses (if usage limits set)
enabledbooleanWhether the key is enabled
rolesstring[]?Permissions attached to the key
permissionsstring[]?Permissions attached to the key
identityobject?Identity info if externalId was set
ratelimitsobject[]?Rate limit states (if rate limiting configured)

Next Steps

Add rate limiting

Limit requests per key

Set usage limits

Cap total requests per key

Add permissions

Fine-grained access control

SDK Reference

Full TypeScript SDK docs

Troubleshooting

  • Ensure the key hasn’t expired or been revoked
  • Verify the Authorization header format: Bearer YOUR_KEY (note the space)
  • Check that your root key has the verify_key permission
  • Check that UNKEY_ROOT_KEY is set correctly in your .env
  • Make sure you’re calling require("dotenv").config() before using env vars
  • Check the Unkey dashboard for any service issues

Build docs developers (and LLMs) love