Back to blog

Monday, March 10, 2025

API Key Authentication for Next.js 15 API Routes

cover

API Key Authentication for Next.js 15 API Routes

Introduction

This documentation provides a step-by-step guide on implementing API Key authentication in a Next.js 15 application using API routes, TypeScript, and Prisma. Developers will learn how to generate API keys, store them securely, and use them to protect API endpoints.

Prerequisites

Before proceeding, ensure you have:

  • A Next.js 15 project with API routes
  • TypeScript configured
  • Prisma set up with a connected database

1. Setting Up API Key Generation

Step 1: Update Prisma Schema

Modify your prisma/schema.prisma file to include an ApiKey model:

model ApiKey {
  id        String   @id @default(cuid())
  userId    String   @unique
  key       String   @unique
  createdAt DateTime @default(now())
  user      User     @relation(fields: [userId], references: [id])
}

Run the following command to apply the changes:

npx prisma migrate dev --name add_api_keys

Step 2: Create API Key Generation Function

Create a utility function to generate secure API keys.

import crypto from "crypto";

export const generateApiKey = (): string => {
  return crypto.randomBytes(32).toString("hex");
};

Step 3: Implement API Key Creation Server Action

Create a new API route at actions/api-keys.ts:

"use server";
import { getAuthenticatedUser } from "@/config/useAuth";
import { generateApiKey } from "@/lib/generateAPIKey";
import { db } from "@/prisma/db";
import { revalidatePath } from "next/cache";
interface APIKeyCreateDTO {
  name: string;
}
export async function createAPIKey(name: string) {
  try {
    const user = await getAuthenticatedUser();
    if (!user) {
      return {
        success: false,
        data: null,
        error: "Your Not Authorized",
      };
    }
    const apiKey = generateApiKey();

    const existingKey = await db.apiKey.findUnique({
      where: { key: apiKey },
    });

    if (existingKey) {
      return {
        success: false,
        data: null,
        error: "This Key Already exists",
      };
    }
    console.log(apiKey);
    const newApiKey = await db.apiKey.create({
      data: {
        orgId: user.orgId,
        key: apiKey,
        name,
      },
    });
    revalidatePath("/dashboard/integrations/api");

    return {
      success: true,
      data: newApiKey,
      error: null,
    };
  } catch (error) {
    console.log(error);
    return {
      success: false,
      data: null,
      error: "Something went wrong",
    };
  }
}

export async function getOrgApiKeys(orgId: string) {
  try {
    const keys = await db.apiKey.findMany({
      orderBy: {
        createdAt: "desc",
      },
    });
    return keys;
  } catch (error) {
    console.log(error);
    return [];
  }
}
export async function deleteAPIKey(id: string) {
  try {
    await db.apiKey.delete({
      where: {
        id,
      },
    });
    revalidatePath("/dashboard/integrations/api");
    return {
      success: true,
    };
  } catch (error) {
    console.log(error);
    return {
      success: false,
    };
  }
}

Create the UI Page for Managing the Keys

  • Create the Page to display the Keys
import { getOrgApiKeys } from "@/actions/apiKeys";
import { ApiKeyManagement } from "@/components/dashboard/api-key-management";
import { getAuthenticatedUser } from "@/config/useAuth";

export default async function Home() {
  const user = await getAuthenticatedUser();
  const apiKeys = await getOrgApiKeys(user.orgId);
  return (
    <main className="container mx-auto py-8 px-4">
      <ApiKeyManagement orgKeys={apiKeys} />
    </main>
  );
}

Create a component that is displaying and managing

"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";
import {
  Copy,
  Key,
  Loader2,
  MoreHorizontal,
  Plus,
  Shield,
  Trash2,
} from "lucide-react";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { toast } from "sonner";
import { createAPIKey, deleteAPIKey } from "@/actions/apiKeys";
import { ApiKey } from "@prisma/client";
import { timeAgo } from "@/lib/timeAgo";

function maskApiKey(key: string): string {
  // Show the "sk_live_" prefix and mask the rest
  const prefix = key.substring(0, 8); // "sk_live_"
  return `${prefix}${"•".repeat(10)}`;
}

export function ApiKeyManagement({ orgKeys }: { orgKeys: ApiKey[] }) {
  const [apiKeys, setApiKeys] = useState<ApiKey[]>(orgKeys);

  const [newKeyName, setNewKeyName] = useState("");
  const [showCreateDialog, setShowCreateDialog] = useState(false);
  const [newlyCreatedKey, setNewlyCreatedKey] = useState<ApiKey | null>(null);
  const [showKeyDialog, setShowKeyDialog] = useState(false);
  const [keyToRevoke, setKeyToRevoke] = useState<string | null>(null);
  const [isCreating, setIsCreating] = useState(false);
  const [isRevoking, setIsRevoking] = useState(false);

  const handleCreateKey = async () => {
    if (!newKeyName.trim()) return;
    setIsCreating(true);
    const { data, error } = await createAPIKey(newKeyName);
    console.log(newKeyName);
    if (error || !data) {
      toast.error("Api Key Failed to Create", {
        description: error,
      });
      setIsCreating(false);
      return;
    }

    const key = data;
    setNewKeyName("");
    setShowCreateDialog(false);
    setNewlyCreatedKey(key);
    setShowKeyDialog(true);
    setIsCreating(false);
  };

  const copyToClipboard = (text: string) => {
    navigator.clipboard.writeText(text);
    toast.success("API key copied to clipboard", {
      description: "The API key has been copied to your clipboard.",
    });
  };
  const handleRevokeKey = async (id: string) => {
    setIsRevoking(true);
    const { success } = await deleteAPIKey(id);
    if (success) {
      setApiKeys(apiKeys.filter((key) => key.id !== id));
      setKeyToRevoke(null);
      toast.success("API key revoked", {
        description: "The API key has been revoked successfully.",
      });
      setIsRevoking(false);
    } else {
      toast.error("API Failed to delete", {
        description: "Something went wrong, Please try again",
      });
      setIsRevoking(false);
    }
  };
  return (
    <div className="space-y-6 max-w-5xl mx-auto">
      <Card className="border-border pb-4">
        <CardHeader className="bg-muted/50 border-b">
          <div className="flex items-center justify-between">
            <div className="flex items-center gap-2">
              <Shield className="h-5 w-5 text-primary" />
              <div>
                <CardTitle className="text-xl">API Keys</CardTitle>
                <CardDescription>
                  Manage your API keys for authentication
                </CardDescription>
              </div>
            </div>
            <Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
              <DialogTrigger asChild>
                <Button>
                  <Plus className="h-4 w-4 mr-2" /> Create new key
                </Button>
              </DialogTrigger>
              <DialogContent>
                <DialogHeader>
                  <DialogTitle>Create a new key</DialogTitle>
                </DialogHeader>
                <div className="space-y-4 py-4">
                  <div className="space-y-2">
                    <Label htmlFor="name">Name</Label>
                    <p className="text-sm text-muted-foreground">
                      Select a name to identify the key in the dashboard.
                    </p>
                    <Input
                      id="name"
                      value={newKeyName}
                      onChange={(e) => setNewKeyName(e.target.value)}
                      placeholder="Enter key name"
                    />
                  </div>
                  {isCreating ? (
                    <Button disabled className="w-full">
                      <Loader2 className="animate-spin" />
                      Creating Please wait...
                    </Button>
                  ) : (
                    <Button
                      className="w-full bg-primary hover:bg-primary/90"
                      onClick={handleCreateKey}
                      disabled={!newKeyName.trim()}
                    >
                      Create Key
                    </Button>
                  )}
                </div>
              </DialogContent>
            </Dialog>
          </div>
        </CardHeader>
        <CardContent className="p-0">
          <Table>
            <TableHeader className="bg-muted/30">
              <TableRow>
                <TableHead>Name</TableHead>
                <TableHead>Key</TableHead>
                <TableHead>Created</TableHead>
                <TableHead className="text-right">Actions</TableHead>
              </TableRow>
            </TableHeader>
            <TableBody>
              {apiKeys.length > 0 ? (
                apiKeys.map((apiKey) => (
                  <TableRow key={apiKey.id}>
                    <TableCell className="font-medium flex items-center gap-2">
                      <Key className="h-4 w-4 text-muted-foreground" />
                      {apiKey.name}
                    </TableCell>
                    <TableCell className="font-mono text-sm">
                      {maskApiKey(apiKey.key)}
                    </TableCell>
                    <TableCell className="text-muted-foreground text-sm">
                      {timeAgo(apiKey.createdAt)}
                    </TableCell>
                    <TableCell className="text-right">
                      <div className="flex justify-end gap-2">
                        <Button
                          variant="outline"
                          size="sm"
                          onClick={() => copyToClipboard(apiKey.key)}
                          className="h-8 text-xs"
                        >
                          <Copy className="h-3.5 w-3.5 mr-1" />
                          Copy
                        </Button>
                        <Button
                          variant="outline"
                          size="sm"
                          onClick={() => setKeyToRevoke(apiKey.id)}
                          className="h-8 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
                        >
                          <Trash2 className="h-3.5 w-3.5 mr-1" />
                          Revoke
                        </Button>
                      </div>
                    </TableCell>
                  </TableRow>
                ))
              ) : (
                <TableRow>
                  <TableCell colSpan={4} className="h-32 text-center">
                    <div className="flex flex-col items-center justify-center gap-3 py-8">
                      <div className="rounded-full bg-muted p-3">
                        <Key className="h-6 w-6 text-muted-foreground" />
                      </div>
                      <div className="space-y-1 text-center">
                        <p className="font-medium">No API Keys Yet</p>
                        <p className="text-sm text-muted-foreground max-w-sm mx-auto">
                          Create your first API key to authenticate requests to
                          our API.
                        </p>
                      </div>
                      <Dialog>
                        <DialogTrigger asChild>
                          <Button className="mt-2">
                            <Plus className="h-4 w-4 mr-2" /> Create key
                          </Button>
                        </DialogTrigger>
                        <DialogContent>
                          <DialogHeader>
                            <DialogTitle>Create a new key</DialogTitle>
                          </DialogHeader>
                          <div className="space-y-4 py-4">
                            <div className="space-y-2">
                              <Label htmlFor="name">Name</Label>
                              <p className="text-sm text-muted-foreground">
                                Select a name to identify the key in the
                                dashboard.
                              </p>
                              <Input
                                id="name"
                                value={newKeyName}
                                onChange={(e) => setNewKeyName(e.target.value)}
                                placeholder="Enter key name"
                              />
                            </div>
                            {isCreating ? (
                              <Button className="w-full" disabled>
                                <Loader2 className="animate-spin" />
                                Creating Please wait...
                              </Button>
                            ) : (
                              <Button
                                className="w-full bg-primary hover:bg-primary/90"
                                onClick={handleCreateKey}
                                disabled={!newKeyName.trim()}
                              >
                                Create Key
                              </Button>
                            )}
                          </div>
                        </DialogContent>
                      </Dialog>
                    </div>
                  </TableCell>
                </TableRow>
              )}
            </TableBody>
          </Table>
        </CardContent>
      </Card>

      {/* Dialog to show newly created key */}
      <Dialog open={showKeyDialog} onOpenChange={setShowKeyDialog}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle className="flex items-center gap-2">
              <Shield className="h-5 w-5 text-primary" />
              Your API Key
            </DialogTitle>
          </DialogHeader>
          <div className="space-y-4 py-4">
            <div className="rounded-md bg-amber-50 border border-amber-200 p-3 text-amber-800">
              <p className="text-sm font-medium">
                Make sure to copy your API key now. You won't be able to see it
                again.
              </p>
            </div>
            <div className="bg-muted p-4 rounded-md flex items-center justify-between">
              <code className="text-sm font-mono break-all">
                {newlyCreatedKey?.key}
              </code>
              <Button
                variant="outline"
                size="sm"
                className="ml-2 shrink-0"
                onClick={() =>
                  newlyCreatedKey && copyToClipboard(newlyCreatedKey.key)
                }
              >
                <Copy className="h-4 w-4 mr-1" />
                Copy
              </Button>
            </div>
            <Button className="w-full" onClick={() => setShowKeyDialog(false)}>
              Done
            </Button>
          </div>
        </DialogContent>
      </Dialog>
      {/* Revoke confirmation dialog */}
      <Dialog
        open={keyToRevoke !== null}
        onOpenChange={(open) => !open && setKeyToRevoke(null)}
      >
        <DialogContent className="sm:max-w-md">
          <DialogHeader>
            <DialogTitle className="flex items-center gap-2 text-destructive">
              <Trash2 className="h-5 w-5" />
              Revoke API Key
            </DialogTitle>
          </DialogHeader>
          <div className="space-y-4 py-4">
            <div className="rounded-md bg-destructive/10 border border-destructive/20 p-3 text-destructive">
              <p className="text-sm font-medium">
                Are you sure you want to revoke this API key? This action cannot
                be undone.
              </p>
            </div>
            <p className="text-sm text-muted-foreground">
              Once revoked, any applications or services using this key will no
              longer be able to authenticate.
            </p>
            <div className="flex justify-end gap-3 pt-2">
              {isRevoking ? (
                <Button disabled>
                  <Loader2 className="animate-spin" />
                  Revoking Please wait...
                </Button>
              ) : (
                <Button
                  variant="destructive"
                  onClick={() => keyToRevoke && handleRevokeKey(keyToRevoke)}
                >
                  Revoke Key
                </Button>
              )}
              <Button variant="outline" onClick={() => setKeyToRevoke(null)}>
                Cancel
              </Button>
            </div>
          </div>
        </DialogContent>
      </Dialog>
    </div>
  );
}

2. Protecting API Routes with API Keys

Step 4: Secure API Routes

Update API routes to require API key authentication. Example: app/api/products/create/route.ts

import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { headers } from "next/headers";

export async function POST(request: Request) {
  const headersList = await headers();
  const apiKey = headersList.get("x-api-key");

  if (!apiKey) {
    return NextResponse.json({ message: "API Key required" }, { status: 401 });
  }

  const validKey = await prisma.apiKey.findUnique({ where: { key: apiKey } });
  if (!validKey) {
    return NextResponse.json({ message: "Invalid API Key" }, { status: 403 });
  }

  const { name, price } = await request.json();
  if (!name || !price) {
    return NextResponse.json(
      { message: "Name and Price are required" },
      { status: 400 }
    );
  }

  const product = await prisma.product.create({
    data: { name, price, userId: validKey.userId },
  });

  return NextResponse.json(product, { status: 201 });
}

3. Using the API with API Key

Developers can now make authenticated requests using the API key. Below is a sample request using fetch:

const apiKey = "YOUR_API_KEY_HERE";
const createProduct = async () => {
  const response = await fetch("/api/products/create", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": apiKey,
    },
    body: JSON.stringify({ name: "Product 1", price: 100 }),
  });

  const data = await response.json();
  console.log(data);
};

createProduct();

Alternatively, using axios:

import axios from "axios";

const apiKey = "YOUR_API_KEY_HERE";

const createProduct = async () => {
  try {
    const response = await axios.post(
      "/api/products/create",
      {
        name: "Product 1",
        price: 100,
      },
      {
        headers: { "x-api-key": apiKey },
      }
    );

    console.log(response.data);
  } catch (error) {
    console.error(error.response?.data || error.message);
  }
};

createProduct();