Upstash Redis et Next.js : Rate Limiting, Caching et Files de Messages

Les applications web modernes font face à trois défis récurrents : protéger les API contre les abus, accélérer les réponses serveur et traiter des opérations en arrière-plan de manière fiable. Redis est la solution classique pour ces trois cas — mais héberger et maintenir un serveur Redis en production demande du temps et de la complexité opérationnelle.
Upstash Redis élimine cette friction en proposant un Redis serverless, facturé à la requête, avec un SDK TypeScript natif. Pas de serveur à gérer, pas de connexion persistante à maintenir — chaque appel passe par HTTP, ce qui le rend parfait pour les environnements serverless comme Vercel, Cloudflare Workers ou AWS Lambda.
Dans ce tutoriel, vous allez construire une API de gestion de contacts avec Next.js qui intègre trois patterns Redis essentiels :
- Rate limiting — limiter les requêtes par IP pour protéger vos endpoints
- Caching — mettre en cache les réponses API pour réduire la latence et la charge base de données
- Files de messages — traiter des tâches asynchrones avec QStash (le service de messaging serverless de Upstash)
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé
- Un compte Upstash gratuit — créez un compte sur console.upstash.com
- Des connaissances de base en React et TypeScript
- Une familiarité avec Next.js App Router (routes API, Server Actions)
- Un éditeur de code (VS Code recommandé)
Ce que vous allez construire
Un système de gestion de contacts avec :
- Rate limiting par IP — bloque les clients qui dépassent 10 requêtes par minute
- Cache intelligent — stocke les résultats de requêtes fréquentes avec invalidation automatique
- File de traitement — envoie des emails de bienvenue via QStash quand un contact est créé
- Dashboard de monitoring — visualise les métriques Redis en temps réel
Étape 1 : Créer le projet Next.js
Initialisez un nouveau projet Next.js avec TypeScript et le App Router :
npx create-next-app@latest upstash-contacts --typescript --tailwind --app --src-dir
cd upstash-contactsInstallez les dépendances Upstash :
npm install @upstash/redis @upstash/ratelimit @upstash/qstashLe package @upstash/redis fournit le client Redis HTTP. @upstash/ratelimit offre des algorithmes de rate limiting prêts à utiliser. @upstash/qstash gère les files de messages serverless.
Étape 2 : Configurer Upstash Redis
Connectez-vous à la console Upstash et créez une nouvelle base de données Redis. Choisissez la région la plus proche de votre serveur de déploiement (par exemple eu-west-1 pour un déploiement européen).
Copiez les variables de connexion et ajoutez-les à votre fichier .env.local :
UPSTASH_REDIS_REST_URL=https://your-instance.upstash.io
UPSTASH_REDIS_REST_TOKEN=AXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
QSTASH_TOKEN=eyJxxxxxxxxxxxxxxxxxxxxxxxxx
QSTASH_CURRENT_SIGNING_KEY=sig_xxxxxxxxxxxxxxxx
QSTASH_NEXT_SIGNING_KEY=sig_xxxxxxxxxxxxxxxxCréez maintenant le fichier de configuration Redis partagé :
// src/lib/redis.ts
import { Redis } from "@upstash/redis";
export const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});Ce client utilise le protocole HTTP — pas de connexion TCP persistante. Chaque appel est une requête REST indépendante, ce qui est idéal pour les fonctions serverless qui peuvent être créées et détruites à tout moment.
Étape 3 : Implémenter le Rate Limiting
Le rate limiting protège vos API contre les abus et les attaques par déni de service. Upstash fournit un module dédié avec plusieurs algorithmes.
Créer le rate limiter
// src/lib/rate-limit.ts
import { Ratelimit } from "@upstash/ratelimit";
import { redis } from "./redis";
// Algorithme "sliding window" : 10 requêtes par minute par identifiant
export const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, "1 m"),
analytics: true, // Active le suivi sur le dashboard Upstash
prefix: "ratelimit:contacts-api",
});Le sliding window (fenêtre glissante) est le meilleur choix pour la plupart des cas. Contrairement au fixed window qui réinitialise brutalement le compteur, le sliding window distribue les requêtes uniformément dans le temps.
Upstash propose trois algorithmes :
- Fixed Window — simple compteur qui se réinitialise à intervalle fixe
- Sliding Window — lissage entre deux fenêtres pour éviter les pics
- Token Bucket — permet des rafales contrôlées, idéal pour les API avec des patterns de trafic irréguliers
Créer le middleware de rate limiting
// src/middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const ratelimit = new Ratelimit({
redis: new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
}),
limiter: Ratelimit.slidingWindow(10, "1 m"),
analytics: true,
prefix: "ratelimit:api",
});
export async function middleware(request: NextRequest) {
// Appliquer uniquement aux routes API
if (!request.nextUrl.pathname.startsWith("/api")) {
return NextResponse.next();
}
const ip = request.headers.get("x-forwarded-for") ?? "127.0.0.1";
const { success, limit, remaining, reset } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json(
{ error: "Trop de requêtes. Réessayez plus tard." },
{
status: 429,
headers: {
"X-RateLimit-Limit": limit.toString(),
"X-RateLimit-Remaining": remaining.toString(),
"X-RateLimit-Reset": reset.toString(),
"Retry-After": Math.ceil((reset - Date.now()) / 1000).toString(),
},
}
);
}
const response = NextResponse.next();
response.headers.set("X-RateLimit-Limit", limit.toString());
response.headers.set("X-RateLimit-Remaining", remaining.toString());
response.headers.set("X-RateLimit-Reset", reset.toString());
return response;
}
export const config = {
matcher: "/api/:path*",
};Ce middleware intercepte toutes les requêtes vers /api/*, vérifie le rate limit par IP, et retourne un code 429 Too Many Requests si le client a dépassé la limite. Les en-têtes X-RateLimit-* permettent au client de savoir combien de requêtes il lui reste.
Étape 4 : Implémenter le Caching
Le caching réduit la latence et la charge sur votre base de données. Upstash Redis est idéal pour cela grâce à son TTL (Time To Live) intégré et sa compatibilité avec les structures de données Redis.
Pattern Cache-Aside
Le pattern le plus courant est le cache-aside : vérifier le cache avant de requêter la source, et stocker le résultat dans le cache pour les prochaines requêtes.
// src/lib/cache.ts
import { redis } from "./redis";
interface CacheOptions {
ttl?: number; // Durée de vie en secondes (défaut : 60)
prefix?: string;
}
export async function cached<T>(
key: string,
fetcher: () => Promise<T>,
options: CacheOptions = {}
): Promise<T> {
const { ttl = 60, prefix = "cache" } = options;
const cacheKey = `${prefix}:${key}`;
// 1. Vérifier le cache
const cached = await redis.get<T>(cacheKey);
if (cached !== null) {
return cached;
}
// 2. Exécuter le fetcher si pas en cache
const data = await fetcher();
// 3. Stocker dans le cache avec TTL
await redis.set(cacheKey, JSON.stringify(data), { ex: ttl });
return data;
}
export async function invalidateCache(pattern: string): Promise<void> {
// Récupérer toutes les clés correspondant au pattern
const keys = await redis.keys(`cache:${pattern}*`);
if (keys.length > 0) {
await redis.del(...keys);
}
}Utiliser le cache dans une route API
// src/app/api/contacts/route.ts
import { NextRequest, NextResponse } from "next/server";
import { cached, invalidateCache } from "@/lib/cache";
// Simuler une base de données
const db = {
contacts: [
{ id: "1", name: "Amine Ben Ali", email: "amine@example.com" },
{ id: "2", name: "Sara Trabelsi", email: "sara@example.com" },
{ id: "3", name: "Youssef Hamdi", email: "youssef@example.com" },
],
};
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get("q") || "";
const contacts = await cached(
`contacts:list:${query}`,
async () => {
// Simuler une requête lente à la base de données
await new Promise((resolve) => setTimeout(resolve, 500));
if (query) {
return db.contacts.filter(
(c) =>
c.name.toLowerCase().includes(query.toLowerCase()) ||
c.email.toLowerCase().includes(query.toLowerCase())
);
}
return db.contacts;
},
{ ttl: 30 } // Cache pendant 30 secondes
);
return NextResponse.json({ contacts });
}
export async function POST(request: NextRequest) {
const body = await request.json();
const { name, email } = body;
if (!name || !email) {
return NextResponse.json(
{ error: "Nom et email requis" },
{ status: 400 }
);
}
const newContact = {
id: Date.now().toString(),
name,
email,
};
db.contacts.push(newContact);
// Invalider le cache après une modification
await invalidateCache("contacts:");
return NextResponse.json({ contact: newContact }, { status: 201 });
}Le point clé est l'invalidation du cache lors des écritures. Sans cela, les utilisateurs verraient des données obsolètes. Le pattern invalidateCache("contacts:") supprime toutes les entrées de cache dont la clé commence par cache:contacts:.
Caching avancé : Stale-While-Revalidate
Pour les données qui changent fréquemment mais tolèrent une légère désynchronisation, le pattern stale-while-revalidate offre le meilleur compromis entre performance et fraîcheur :
// src/lib/swr-cache.ts
import { redis } from "./redis";
interface SWRCacheOptions {
ttl: number; // Durée de vie "fraîche" en secondes
staleFor: number; // Durée supplémentaire où la donnée est servie mais revalidée
prefix?: string;
}
interface CachedEntry<T> {
data: T;
cachedAt: number;
}
export async function swrCached<T>(
key: string,
fetcher: () => Promise<T>,
options: SWRCacheOptions
): Promise<T> {
const { ttl, staleFor, prefix = "swr" } = options;
const cacheKey = `${prefix}:${key}`;
const entry = await redis.get<CachedEntry<T>>(cacheKey);
if (entry) {
const age = (Date.now() - entry.cachedAt) / 1000;
if (age <= ttl) {
// Données fraîches — servir directement
return entry.data;
}
if (age <= ttl + staleFor) {
// Données périmées mais acceptables — servir et revalider en arrière-plan
revalidate(cacheKey, fetcher, ttl + staleFor);
return entry.data;
}
}
// Pas de cache ou trop vieux — fetch et stocker
const data = await fetcher();
await redis.set(
cacheKey,
JSON.stringify({ data, cachedAt: Date.now() }),
{ ex: ttl + staleFor }
);
return data;
}
async function revalidate<T>(
key: string,
fetcher: () => Promise<T>,
totalTtl: number
): Promise<void> {
try {
const data = await fetcher();
await redis.set(
key,
JSON.stringify({ data, cachedAt: Date.now() }),
{ ex: totalTtl }
);
} catch {
// Silencieux — la donnée périmée est déjà servie
}
}Étape 5 : Files de Messages avec QStash
QStash est le service de messaging serverless de Upstash. Il permet de déclencher des tâches en arrière-plan sans serveur dédié — QStash envoie une requête HTTP vers votre endpoint quand il faut exécuter la tâche.
Configurer QStash
// src/lib/qstash.ts
import { Client } from "@upstash/qstash";
export const qstash = new Client({
token: process.env.QSTASH_TOKEN!,
});Publier un message dans la file
Quand un nouveau contact est créé, on envoie un message à QStash pour déclencher un email de bienvenue :
// src/app/api/contacts/route.ts (mise à jour du POST)
import { qstash } from "@/lib/qstash";
export async function POST(request: NextRequest) {
const body = await request.json();
const { name, email } = body;
if (!name || !email) {
return NextResponse.json(
{ error: "Nom et email requis" },
{ status: 400 }
);
}
const newContact = {
id: Date.now().toString(),
name,
email,
};
db.contacts.push(newContact);
await invalidateCache("contacts:");
// Publier un message pour envoyer un email de bienvenue
await qstash.publishJSON({
url: `${process.env.NEXT_PUBLIC_APP_URL}/api/webhooks/welcome-email`,
body: {
contactId: newContact.id,
name: newContact.name,
email: newContact.email,
},
retries: 3,
delay: 5, // Attendre 5 secondes avant le premier essai
});
return NextResponse.json({ contact: newContact }, { status: 201 });
}Créer le webhook consommateur
// src/app/api/webhooks/welcome-email/route.ts
import { NextRequest, NextResponse } from "next/server";
import { verifySignatureAppRouter } from "@upstash/qstash/nextjs";
async function handler(request: NextRequest) {
const body = await request.json();
const { contactId, name, email } = body;
console.log(`Envoi de l'email de bienvenue à ${name} (${email})`);
// Ici, intégrez votre service d'email (Resend, SendGrid, etc.)
// Exemple avec une simulation :
await simulateSendEmail({
to: email,
subject: `Bienvenue ${name} !`,
body: `Merci de nous avoir rejoints. Votre compte est prêt.`,
});
return NextResponse.json({ success: true });
}
async function simulateSendEmail(params: {
to: string;
subject: string;
body: string;
}) {
// Simuler un délai d'envoi
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log(`Email envoyé à ${params.to}: ${params.subject}`);
}
// Vérifier la signature QStash pour sécuriser le webhook
export const POST = verifySignatureAppRouter(handler);La fonction verifySignatureAppRouter est cruciale — elle vérifie que la requête provient bien de QStash et non pas d'un attaquant. QStash signe chaque message avec les clés définies dans vos variables d'environnement.
Planifier des tâches récurrentes
QStash supporte aussi les tâches planifiées (cron). Par exemple, envoyer un rapport quotidien :
// src/lib/scheduled-tasks.ts
import { qstash } from "./qstash";
export async function setupDailyReport() {
await qstash.schedules.create({
destination: `${process.env.NEXT_PUBLIC_APP_URL}/api/webhooks/daily-report`,
cron: "0 9 * * *", // Tous les jours à 9h
retries: 3,
});
}// src/app/api/webhooks/daily-report/route.ts
import { NextRequest, NextResponse } from "next/server";
import { verifySignatureAppRouter } from "@upstash/qstash/nextjs";
import { redis } from "@/lib/redis";
async function handler(request: NextRequest) {
// Collecter les métriques de la journée
const totalRequests = await redis.get<number>("metrics:total-requests") || 0;
const newContacts = await redis.get<number>("metrics:new-contacts") || 0;
const rateLimitHits = await redis.get<number>("metrics:rate-limit-hits") || 0;
const report = {
date: new Date().toISOString().split("T")[0],
totalRequests,
newContacts,
rateLimitHits,
};
console.log("Rapport quotidien:", report);
// Réinitialiser les compteurs
await redis.set("metrics:total-requests", 0);
await redis.set("metrics:new-contacts", 0);
await redis.set("metrics:rate-limit-hits", 0);
return NextResponse.json({ report });
}
export const POST = verifySignatureAppRouter(handler);Étape 6 : Construire le Dashboard de Monitoring
Créez un composant React pour visualiser les métriques Redis en temps réel.
Route API pour les métriques
// src/app/api/metrics/route.ts
import { NextResponse } from "next/server";
import { redis } from "@/lib/redis";
export async function GET() {
const [totalRequests, newContacts, rateLimitHits, cacheKeys] =
await Promise.all([
redis.get<number>("metrics:total-requests"),
redis.get<number>("metrics:new-contacts"),
redis.get<number>("metrics:rate-limit-hits"),
redis.keys("cache:*"),
]);
return NextResponse.json({
totalRequests: totalRequests || 0,
newContacts: newContacts || 0,
rateLimitHits: rateLimitHits || 0,
cachedEntries: cacheKeys.length,
uptime: process.uptime(),
});
}Composant Dashboard
// src/app/dashboard/page.tsx
"use client";
import { useEffect, useState } from "react";
interface Metrics {
totalRequests: number;
newContacts: number;
rateLimitHits: number;
cachedEntries: number;
uptime: number;
}
export default function DashboardPage() {
const [metrics, setMetrics] = useState<Metrics | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchMetrics = async () => {
try {
const res = await fetch("/api/metrics");
const data = await res.json();
setMetrics(data);
} catch (err) {
console.error("Erreur lors du chargement des métriques:", err);
} finally {
setLoading(false);
}
};
fetchMetrics();
const interval = setInterval(fetchMetrics, 5000); // Rafraîchir toutes les 5 secondes
return () => clearInterval(interval);
}, []);
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<p className="text-gray-500">Chargement des métriques...</p>
</div>
);
}
return (
<div className="max-w-4xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-8">Dashboard Redis</h1>
<div className="grid grid-cols-2 gap-6 md:grid-cols-4">
<MetricCard
label="Requêtes totales"
value={metrics?.totalRequests ?? 0}
color="blue"
/>
<MetricCard
label="Nouveaux contacts"
value={metrics?.newContacts ?? 0}
color="green"
/>
<MetricCard
label="Rate limit atteint"
value={metrics?.rateLimitHits ?? 0}
color="red"
/>
<MetricCard
label="Entrées en cache"
value={metrics?.cachedEntries ?? 0}
color="purple"
/>
</div>
</div>
);
}
function MetricCard({
label,
value,
color,
}: {
label: string;
value: number;
color: string;
}) {
const colorClasses: Record<string, string> = {
blue: "bg-blue-50 border-blue-200 text-blue-700",
green: "bg-green-50 border-green-200 text-green-700",
red: "bg-red-50 border-red-200 text-red-700",
purple: "bg-purple-50 border-purple-200 text-purple-700",
};
return (
<div className={`rounded-xl border-2 p-6 ${colorClasses[color]}`}>
<p className="text-sm font-medium opacity-75">{label}</p>
<p className="text-3xl font-bold mt-2">{value.toLocaleString()}</p>
</div>
);
}Étape 7 : Tester le système complet
Tester le rate limiting
Utilisez curl pour envoyer des requêtes rapides et vérifier que le rate limiter fonctionne :
# Envoyer 12 requêtes rapidement
for i in $(seq 1 12); do
echo "Requête $i:"
curl -s -o /dev/null -w "HTTP %{http_code} | Remaining: " \
http://localhost:3000/api/contacts
echo ""
doneVous devriez voir les 10 premières requêtes retourner 200 et les deux dernières 429.
Tester le caching
# Première requête — pas de cache (lente)
time curl http://localhost:3000/api/contacts
# Deuxième requête — depuis le cache (rapide)
time curl http://localhost:3000/api/contactsLa première requête devrait prendre environ 500 ms (simulation de la base de données), et la deuxième devrait être quasi instantanée.
Tester les files de messages
# Créer un contact — déclenche un email via QStash
curl -X POST http://localhost:3000/api/contacts \
-H "Content-Type: application/json" \
-d '{"name": "Fatma Cherif", "email": "fatma@example.com"}'Vérifiez les logs de votre application pour voir le message "Email envoyé" apparaître quelques secondes après la création.
Dépannage
Erreur "UPSTASH_REDIS_REST_URL is not defined"
Vérifiez que le fichier .env.local existe à la racine du projet et contient les bonnes variables. Redémarrez le serveur de développement après toute modification de .env.local.
Le rate limiter ne bloque pas les requêtes en développement
En mode développement, le x-forwarded-for peut être vide. Le middleware utilise 127.0.0.1 comme fallback, ce qui signifie que toutes les requêtes locales partagent le même compteur. Ce comportement est correct.
Les webhooks QStash échouent en local
QStash a besoin de pouvoir atteindre votre serveur via Internet. Pour le développement local, utilisez un tunnel comme ngrok :
npx ngrok http 3000Puis mettez à jour NEXT_PUBLIC_APP_URL dans .env.local avec le lien ngrok.
Le cache ne se vide pas après une écriture
Vérifiez que la fonction invalidateCache utilise le bon préfixe. Les clés Redis sont sensibles à la casse.
Bonnes pratiques de production
1. Utiliser des préfixes de clés cohérents
Organisez vos clés Redis avec des préfixes descriptifs pour faciliter le monitoring et le débogage :
ratelimit:api:{ip}
cache:contacts:list:{query}
swr:dashboard:metrics
metrics:total-requests
2. Définir des TTL appropriés
- Rate limiting : TTL court (1 à 5 minutes)
- Cache de données : TTL moyen (30 secondes à 5 minutes)
- Sessions : TTL long (24 heures à 7 jours)
- Données temporaires : TTL très court (5 à 30 secondes)
3. Gérer les erreurs Redis gracieusement
Redis ne devrait jamais être un point de défaillance critique. Si Redis est indisponible, votre application doit continuer à fonctionner :
export async function cachedWithFallback<T>(
key: string,
fetcher: () => Promise<T>,
ttl: number = 60
): Promise<T> {
try {
const cached = await redis.get<T>(`cache:${key}`);
if (cached) return cached;
} catch {
// Redis indisponible — continuer sans cache
console.warn("Redis indisponible, exécution sans cache");
}
const data = await fetcher();
try {
await redis.set(`cache:${key}`, JSON.stringify(data), { ex: ttl });
} catch {
// Silencieux — la donnée est quand même retournée
}
return data;
}4. Surveiller la consommation
Le plan gratuit Upstash offre 10 000 requêtes par jour. Pour les applications en production :
- Utilisez le dashboard Upstash pour surveiller la consommation
- Configurez des alertes de dépassement
- Optimisez le nombre de commandes par opération (utilisez les pipelines Redis)
Déploiement sur Vercel
Le déploiement est simple grâce à la nature serverless de Upstash :
- Poussez votre code sur GitHub
- Connectez le dépôt à Vercel
- Ajoutez les variables d'environnement dans les settings Vercel
- Déployez
Upstash propose aussi une intégration native Vercel qui configure automatiquement les variables d'environnement :
# Installer l'intégration Upstash depuis le marketplace Vercel
# Les variables UPSTASH_REDIS_REST_URL et UPSTASH_REDIS_REST_TOKEN
# seront automatiquement ajoutées à votre projetProchaines étapes
Maintenant que vous maîtrisez les bases de Upstash Redis avec Next.js, voici quelques pistes pour aller plus loin :
- Gestion de sessions — remplacez les sessions basées sur des cookies par des sessions Redis pour une meilleure scalabilité
- Leaderboards et classements — utilisez les Sorted Sets Redis pour des classements en temps réel
- Pub/Sub en temps réel — combinez Upstash Redis avec Server-Sent Events pour du push en temps réel
- Cache de pages entières — mettez en cache les réponses HTML pour les pages statiques dynamiques
- Intégration avec Drizzle ORM — combinez le caching Redis avec votre couche d'accès aux données
Conclusion
Vous avez appris à intégrer Upstash Redis dans une application Next.js pour résoudre trois problèmes courants : le rate limiting, le caching et les files de messages. L'approche serverless de Upstash élimine la complexité opérationnelle — pas de serveur Redis à gérer, pas de connexion persistante à maintenir, et une facturation à la requête qui rend le service accessible même pour les petits projets.
Les patterns présentés dans ce tutoriel — cache-aside, stale-while-revalidate, et les webhooks vérifiés — sont des fondations solides pour toute application en production. Combinez-les selon vos besoins : rate limiting pour protéger, caching pour accélérer, et files de messages pour distribuer le travail.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Construire des tâches de fond en production avec Trigger.dev v3 et Next.js
Apprenez à construire des tâches de fond fiables, des tâches planifiées et des workflows multi-étapes avec Trigger.dev v3 et Next.js. Ce tutoriel couvre la création de tâches, la gestion des erreurs, les retries, les cron jobs et le déploiement en production.

Construire un Agent IA Autonome avec Agentic RAG et Next.js
Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.

Better Auth avec Next.js 15 : Le Guide Complet d'Authentification pour 2026
Apprenez à implémenter une authentification complète dans Next.js 15 avec Better Auth. Ce tutoriel couvre email/mot de passe, OAuth, sessions, protection des routes et contrôle d'accès basé sur les rôles.