Toute API publique finit par avoir besoin de la même tuyauterie : un moyen d'émettre des clés aux clients, un moyen de les vérifier à chaque requête, des limites de débit par clé pour stopper les abus, des quotas d'usage liés aux plans de facturation, et des permissions fines. Construire tout cela soi-même implique une table de clés, une logique de hachage, un cluster Redis pour les compteurs, un pipeline d'analytique et un tableau de bord. C'est plusieurs semaines de travail sans valeur différenciante.
Unkey est une plateforme open-source de gestion de clés API et de limitation de débit qui réduit tout cela à quelques appels de SDK. Vous créez une clé, vous vérifiez une clé, et Unkey gère le hachage, la vérification globale à faible latence, la limitation de débit, les crédits basés sur l'usage, les rôles, les permissions et l'analytique. Le projet est entièrement auto-hébergeable, ce qui compte pour les équipes des marchés réglementés du MENA opérant sous les règles de résidence des données de l'INPDP et de la PDPL.
Dans ce tutoriel, vous allez construire une API de traitement de documents pour un SaaS fictif où chaque client s'authentifie avec une clé API. Vous émettrez des clés par programmation, protégerez des routes avec l'assistant pour Next.js, appliquerez des limites de débit et des quotas de crédits par clé, et verrouillerez des points de terminaison premium derrière des permissions.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé
- Un compte Unkey gratuit — inscrivez-vous sur app.unkey.com
- Des connaissances de base en React et TypeScript
- Une familiarité avec l'App Router de Next.js (Route Handlers, middleware)
- Un éditeur de code (VS Code recommandé)
Ce que vous allez construire
Une API de documents SaaS avec :
- Émission de clés par programmation — générer une clé API à portée définie lors de l'inscription d'un client
- Vérification des requêtes — valider chaque clé entrante en un seul appel
- Limitation de débit par clé — réguler chaque client indépendamment
- Crédits d'usage — mesurer les appels d'API selon un quota de plan avec recharge automatique
- Permissions basées sur les rôles — restreindre les points de terminaison premium aux clés portant la bonne permission
- Limitation de débit autonome — protéger les routes non authentifiées par IP
Comment fonctionne Unkey
Trois concepts portent tout le système :
- Clé racine (root key) — une clé privilégiée que votre backend utilise pour appeler l'API de gestion d'Unkey (créer, vérifier, révoquer). Elle ne quitte jamais votre serveur et vit dans une variable d'environnement.
- API — un espace de noms dans Unkey qui regroupe toutes les clés que vous émettez. Chaque API possède un
apiId. - Clés clients — les clés que vous remettez à vos utilisateurs. Unkey ne stocke qu'un hachage ; le texte en clair est affiché exactement une seule fois à la création.
La règle d'or : la vérification renvoie toujours un HTTP 200. Une clé rejetée n'est pas une erreur de transport — vous devez inspecter le champ valid (et le code) dans le corps de la réponse pour décider d'autoriser ou non la requête.
Étape 1 : Créer le projet Next.js
Initialisez un nouveau projet Next.js avec TypeScript et l'App Router :
npx create-next-app@latest unkey-docs-api --typescript --app --src-dir --eslint
cd unkey-docs-apiInstallez les SDK Unkey :
npm install @unkey/api @unkey/nextjs @unkey/ratelimit@unkey/api— le SDK de gestion pour créer et vérifier des clés depuis votre backend@unkey/nextjs— un enrobage léger qui vérifie les clés à l'intérieur des Route Handlers@unkey/ratelimit— limitation de débit autonome pour les routes sans clé API (connexion, inscription, points de terminaison publics)
Étape 2 : Configurer votre espace de travail Unkey
Dans le tableau de bord Unkey :
- Créez une nouvelle API (nommez-la
documents-api). Copiez son API ID — il ressemble àapi_3xZ.... - Allez dans Settings → Root Keys et créez une clé racine avec au minimum ces permissions :
api.*.create_key,api.*.verify_keyetratelimit.*.limit. Copiez-la — elle n'est affichée qu'une seule fois.
Créez un fichier .env.local :
UNKEY_ROOT_KEY=unkey_3y...
UNKEY_API_ID=api_3xZ...Ne validez jamais ce fichier dans Git. La clé racine est aussi sensible qu'un mot de passe de base de données — quiconque la détient peut forger des clés contre votre API.
Étape 3 : Émettre une clé API à l'inscription d'un client
Lorsqu'un nouveau client rejoint, votre backend demande à Unkey de forger une clé limitée à son compte. Créez un assistant côté serveur dans src/lib/unkey.ts :
import { Unkey } from "@unkey/api";
// Une seule instance partagée, réutilisée dans toute l'application.
export const unkey = new Unkey({
rootKey: process.env.UNKEY_ROOT_KEY!,
});
interface IssueKeyInput {
userId: string;
plan: "free" | "pro" | "enterprise";
}
// Associer un plan de facturation à un quota mensuel de requêtes.
const PLAN_CREDITS = {
free: 1_000,
pro: 50_000,
enterprise: 1_000_000,
} as const;
export async function issueApiKey({ userId, plan }: IssueKeyInput) {
const result = await unkey.keys.createKey({
apiId: process.env.UNKEY_API_ID!,
prefix: "docs", // les clés ressemblent à docs_xxxxxxxx — faciles à reconnaître
name: `clé ${plan} pour ${userId}`,
externalId: userId, // relie la clé à votre propre enregistrement utilisateur
meta: { plan },
// Limite de débit par plan, appliquée automatiquement à chaque vérification.
ratelimits: [
{
name: "requests",
limit: plan === "enterprise" ? 100 : plan === "pro" ? 50 : 10,
duration: 10_000, // toutes les 10 secondes
autoApply: true,
},
],
// Quota d'usage mensuel qui se recharge le 1er de chaque mois.
credits: {
remaining: PLAN_CREDITS[plan],
refill: {
interval: "monthly",
amount: PLAN_CREDITS[plan],
refillDay: 1,
},
},
// Les clients premium reçoivent aussi la permission "documents.export".
permissions:
plan === "enterprise"
? ["documents.read", "documents.write", "documents.export"]
: ["documents.read", "documents.write"],
});
// La clé en clair est renvoyée UNE SEULE fois. Retournez-la à l'appelant
// maintenant — vous ne pourrez plus jamais la lire.
return result;
}Quelques détails à comprendre :
externalIdrelie la clé Unkey à votre propreuserId. Plus tard, vous pourrez lister ou révoquer chaque clé appartenant à un utilisateur sans stocker la clé vous-même.autoApply: truesur une limite de débit signifie qu'Unkey l'applique automatiquement à chaque appelverifyKey— vous n'avez pas à repasser la limite au moment de la vérification.creditstransforment chaque vérification en une unité mesurée. Lorsqueremainingatteint zéro, la clé cesse de valider jusqu'à la prochaine recharge.
Exposez maintenant cela via un Route Handler. Créez src/app/api/keys/route.ts :
import { NextRequest, NextResponse } from "next/server";
import { issueApiKey } from "@/lib/unkey";
export async function POST(req: NextRequest) {
// Dans une vraie application, authentifiez d'abord la session du tableau de bord.
const { userId, plan } = await req.json();
if (!userId || !plan) {
return NextResponse.json(
{ error: "userId et plan sont requis" },
{ status: 400 },
);
}
const { result, error } = await issueApiKey({ userId, plan });
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
// result.key est le texte en clair — présentez-le au client une seule fois.
return NextResponse.json({ key: result.key, keyId: result.keyId });
}Le SDK renvoie une forme { result, error }, vous n'avez donc jamais besoin d'envelopper les appels dans un try/catch pour les erreurs d'API attendues — vous testez error à la place.
Étape 4 : Protéger une route avec withUnkey
Le moyen le plus simple de protéger un Route Handler est l'enrobage withUnkey de @unkey/nextjs. Il lit la clé dans l'en-tête Authorization: Bearer, la vérifie et attache le résultat à req.unkey.
Créez src/app/api/documents/route.ts :
import { withUnkey } from "@unkey/nextjs";
import { NextResponse } from "next/server";
export const POST = withUnkey(
async (req) => {
// withUnkey n'exécute votre handler que si la clé est présente structurellement.
// Vous devez quand même vérifier sa validité vous-même :
if (!req.unkey?.valid) {
return NextResponse.json(
{ error: "Non autorisé", code: req.unkey?.code },
{ status: 401 },
);
}
// req.unkey porte tout ce que vous avez configuré : ownerId, meta, permissions.
const plan = (req.unkey.meta?.plan as string) ?? "free";
return NextResponse.json({
message: "Document accepté pour traitement",
plan,
remaining: req.unkey.remaining, // crédits restants après cet appel
});
},
{
rootKey: process.env.UNKEY_ROOT_KEY!,
},
);Testez-le. Forgez d'abord une clé :
curl -X POST http://localhost:3000/api/keys \
-H "Content-Type: application/json" \
-d '{"userId":"user_42","plan":"pro"}'
# { "key": "docs_3a8...", "keyId": "key_..." }Appelez ensuite la route protégée avec cette clé :
curl -X POST http://localhost:3000/api/documents \
-H "Authorization: Bearer docs_3a8..." \
-H "Content-Type: application/json" \
-d '{"title":"rapport T3"}'
# { "message": "Document accepté pour traitement", "plan": "pro", "remaining": 49999 }Appelez-la sans clé — ou avec n'importe quoi — et vous obtenez un 401. Notez que remaining a diminué de un : le quota de crédits est appliqué automatiquement.
Étape 5 : Vérifier les clés manuellement pour un contrôle total
withUnkey est pratique, mais les vraies API ont souvent besoin de vérifier au sein de leur propre logique — par exemple pour contrôler une permission spécifique, facturer un coût de crédit variable ou appliquer une limite de débit nommée. Utilisez directement unkey.keys.verifyKey.
Créez une garde réutilisable dans src/lib/auth.ts :
import { unkey } from "./unkey";
interface VerifyOptions {
permission?: string; // ex. "documents.export"
cost?: number; // combien de crédits cette requête consomme
}
export async function verifyRequest(
authHeader: string | null,
opts: VerifyOptions = {},
) {
const key = authHeader?.replace(/^Bearer\s+/i, "");
if (!key) {
return { ok: false as const, status: 401, reason: "missing_key" };
}
const { result, error } = await unkey.keys.verifyKey({
key,
// Une requête de permission : la clé doit porter cette permission pour passer.
permissions: opts.permission,
// Facturer plus d'un crédit pour les opérations lourdes.
credits: opts.cost ? { cost: opts.cost } : undefined,
});
if (error) {
// Erreur de transport/gestion (pas une clé rejetée) — fermer par défaut.
return { ok: false as const, status: 500, reason: error.message };
}
if (!result.valid) {
// result.code indique POURQUOI : NOT_FOUND, RATE_LIMITED,
// USAGE_EXCEEDED, INSUFFICIENT_PERMISSIONS, EXPIRED, DISABLED...
const status = result.code === "RATE_LIMITED" ? 429 : 403;
return { ok: false as const, status, reason: result.code };
}
return { ok: true as const, key: result };
}Construisez maintenant un point de terminaison premium qui requiert la permission documents.export et coûte 5 crédits par appel. Créez src/app/api/documents/export/route.ts :
import { NextRequest, NextResponse } from "next/server";
import { verifyRequest } from "@/lib/auth";
export async function POST(req: NextRequest) {
const auth = await verifyRequest(req.headers.get("authorization"), {
permission: "documents.export",
cost: 5, // l'export est coûteux — mesurez-le plus lourdement
});
if (!auth.ok) {
return NextResponse.json(
{ error: auth.reason },
{ status: auth.status },
);
}
// Seules les clés enterprise portent documents.export ; les clés free/pro obtiennent un 403.
return NextResponse.json({
message: "Export démarré",
remaining: auth.key.remaining,
});
}Une clé pro appelant ce point de terminaison reçoit 403 INSUFFICIENT_PERMISSIONS, tandis qu'une clé enterprise réussit et brûle 5 crédits. Le même appel de vérification a appliqué l'authentification, l'autorisation et le quota en un seul aller-retour.
Étape 6 : Limiter le débit des routes non authentifiées par IP
Certaines routes n'ont pas encore de clé API — un formulaire d'inscription public, un point de terminaison de contact, une barre de recherche de documentation. Utilisez le SDK autonome @unkey/ratelimit indexé sur l'IP du client.
Créez src/lib/ratelimit.ts :
import { Ratelimit } from "@unkey/ratelimit";
export const publicLimiter = new Ratelimit({
rootKey: process.env.UNKEY_ROOT_KEY!,
namespace: "public-signup", // isoler cette limite des autres espaces de noms
limit: 5, // 5 tentatives...
duration: "60s", // ...par minute et par identifiant
async: true, // fire-and-forget pour la latence la plus faible
});Appliquez-le dans un Route Handler public dans src/app/api/signup/route.ts :
import { NextRequest, NextResponse } from "next/server";
import { publicLimiter } from "@/lib/ratelimit";
export async function POST(req: NextRequest) {
// Derrière un proxy, préférez l'entrée x-forwarded-for la plus à gauche.
const ip =
req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
const { success, remaining, reset } = await publicLimiter.limit(ip);
if (!success) {
return NextResponse.json(
{ error: "Trop de tentatives d'inscription. Réessayez bientôt." },
{
status: 429,
headers: {
"X-RateLimit-Remaining": remaining.toString(),
"X-RateLimit-Reset": reset.toString(),
},
},
);
}
// ...poursuivre la création du compte
return NextResponse.json({ ok: true });
}Le drapeau async: true fait que le limiteur répond de manière optimiste tout en synchronisant les compteurs en arrière-plan. Il réduit la latence au prix de laisser passer une infime rafale aux marges — un bon compromis pour les formulaires d'inscription, le mauvais pour un point de terminaison de paiement où vous devriez fixer async: false.
Étape 7 : Révoquer et lister les clés
Lorsqu'un client rétrograde, se désabonne ou fait fuiter une clé, vous la révoquez. Comme vous avez fixé externalId à votre userId, vous pouvez aussi énumérer toutes les clés qu'un utilisateur possède.
import { unkey } from "@/lib/unkey";
// Invalider immédiatement une seule clé par son keyId.
export async function revokeKey(keyId: string) {
return unkey.keys.deleteKey({ keyId });
}
// Désactiver temporairement sans supprimer (ex. en cas d'échec de paiement).
export async function suspendKey(keyId: string) {
return unkey.keys.updateKey({ keyId, enabled: false });
}Une clé révoquée ou désactivée échoue à sa toute prochaine vérification avec le code NOT_FOUND ou DISABLED — il n'y a aucun délai de propagation à gérer de votre côté.
Tester votre implémentation
Parcourez le cycle de vie complet depuis un terminal :
# 1. Émettre une clé de plan free
curl -s -X POST http://localhost:3000/api/keys \
-H "Content-Type: application/json" \
-d '{"userId":"user_99","plan":"free"}'
# 2. Marteler le point de terminaison documents au-delà de la limite de 10 par 10 s
for i in $(seq 1 15); do
curl -s -o /dev/null -w "%{http_code}\n" \
-X POST http://localhost:3000/api/documents \
-H "Authorization: Bearer docs_VOTRECLE" \
-H "Content-Type: application/json" -d '{}'
done
# Vous devriez voir les 200 basculer en 429 une fois la fenêtre remplie.
# 3. Essayer le point de terminaison export avec une clé free -> 403
curl -s -X POST http://localhost:3000/api/documents/export \
-H "Authorization: Bearer docs_VOTRECLE"Pour une couverture automatisée, vérifiez les trois branches décisives : une clé valide renvoie 200, une clé limitée renvoie 429 avec code: "RATE_LIMITED", et une clé free atteignant /export renvoie 403 avec code: "INSUFFICIENT_PERMISSIONS".
Dépannage
Chaque vérification renvoie valid: false avec NOT_FOUND. Votre clé racine manque probablement de la permission verify_key, ou vous vérifiez contre la mauvaise API. Confirmez les portées de la clé racine dans le tableau de bord et que la clé a été créée sous le même apiId.
Les limites de débit ne se déclenchent jamais. Vérifiez que la limite de débit sur la clé a bien autoApply: true, ou que vous passez un tableau ratelimits au moment de la vérification. Une limite définie sur la clé mais non auto-appliquée n'est appliquée que lorsque vous la référencez par son nom pendant la vérification.
Les crédits ne diminuent jamais. verifyKey ne consomme des crédits que si la clé a été créée avec un objet credits. Les clés forgées sans cet objet sont illimitées par conception.
Pics de latence sur les routes protégées. La vérification est un appel réseau. Gardez le client Unkey comme singleton au niveau du module (comme à l'étape 3) afin que les connexions soient réutilisées, et préférez un déploiement proche de la périphérie d'Unkey. Pour les limites non critiques, async: true retire l'aller-retour du chemin critique.
Liste de contrôle pour la production
- N'exposez jamais la clé racine au navigateur. Tous les appels de gestion Unkey appartiennent aux Route Handlers ou aux server actions, jamais aux composants clients.
- Fermez par défaut. Si
verifyKeyrenvoie uneerrorde transport, rejetez la requête plutôt que de la laisser passer. - Affichez la clé en clair une seule fois. Rendez-la dans le tableau de bord à la création, puis ne stockez que le
keyIdde votre côté. - Choisissez
asyncdélibérément. Utilisezasync: falselà où la justesse prime sur la latence (facturation, écritures sensibles aux abus). - Auto-hébergez pour la résidence des données. Les équipes sous l'INPDP ou la PDPL peuvent exécuter Unkey sur leur propre infrastructure afin que le matériel des clés et l'analytique d'usage ne quittent jamais la région.
Prochaines étapes
- Ajoutez des rôles à côté des permissions pour gérer l'accès par lots plutôt qu'une permission à la fois.
- Faites remonter l'analytique d'Unkey dans votre tableau de bord client pour montrer les tendances d'usage par clé.
- Combinez cela avec un déploiement auto-hébergé via le workflow déploiement auto-hébergé avec Coolify v4.
- Associez les clés API à la mise en cache en périphérie — voir le tutoriel limitation de débit et mise en cache avec Upstash Redis pour une approche complémentaire.
Conclusion
Vous avez construit une couche complète d'authentification d'API sans écrire une seule ligne de hachage de clés ni de gestion de compteurs. Unkey vous a fourni l'émission de clés par programmation, la vérification en un seul appel, les limites de débit par clé, les crédits d'usage liés aux plans de facturation et le verrouillage par permissions — le tout depuis trois SDK. L'architecture passe à l'échelle d'un projet personnel gratuit à un SaaS d'entreprise, et parce qu'Unkey est open source et auto-hébergeable, elle s'intègre proprement aux exigences de résidence des données sous lesquelles opèrent les équipes du MENA. La prochaine fois que vous lancez une API publique, optez pour une couche de clés gérée plutôt que d'en reconstruire une de zéro.