Avec l'App Router, Next.js a repensé la façon de créer des endpoints HTTP côté serveur. Les Route Handlers remplacent les anciennes API Routes du Pages Router et s'intègrent nativement avec le système de cache, le middleware et les React Server Components. Ce guide couvre les patterns essentiels : gestion des verbes HTTP, webhooks sécurisés, streaming de réponses et typage TypeScript strict.
Anatomie d'un Route Handler
Les Route Handlers se définissent dans un fichier route.ts placé dans le dossier app/. Contrairement aux Server Actions, ils exposent des endpoints HTTP accessibles depuis n'importe quel client — mobile, service tiers ou frontend découplé. Chaque verbe HTTP (GET, POST, PUT, DELETE, PATCH) correspond à une fonction exportée du même nom.
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { db } from '@/lib/db'
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2),
})
export async function POST(request: NextRequest) {
const body = await request.json()
// Validation Zod — ne jamais faire confiance à l'input
const parsed = createUserSchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json(
{ error: parsed.error.flatten() },
{ status: 400 }
)
}
const user = await db.user.create({ data: parsed.data })
return NextResponse.json(user, { status: 201 })
}
Le typage de NextRequest donne accès aux headers, cookies, paramètres d'URL et body de façon typée. NextResponse est une extension de la Response standard Web API — on peut donc retourner n'importe quel objet Response natif à la place.
Paramètres dynamiques et query strings
Les segments dynamiques fonctionnent comme pour les pages. L'objet params est passé en second argument de la fonction handler.
// app/api/users/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const user = await db.user.findUnique({ where: { id } })
if (!user) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
return NextResponse.json(user)
}
Pour les query strings, request.nextUrl.searchParams est l'API recommandée : const page = Number(request.nextUrl.searchParams.get('page') ?? '1'). Le helper nextUrl évite de parser manuellement la chaîne de requête et gère correctement les cas limites (valeurs manquantes, multiples valeurs pour le même paramètre).
Webhooks : vérification de signature et body brut
Les webhooks (Stripe, GitHub, Clerk) exigent le body en format brut pour vérifier la signature HMAC. La plupart des erreurs viennent du fait d'appeler request.json() avant la vérification : cela consomme le stream et rend la validation de signature impossible.
// app/api/webhooks/stripe/route.ts
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function POST(request: NextRequest) {
// Body brut AVANT toute désérialisation
const body = await request.text()
const signature = request.headers.get('stripe-signature')!
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
)
} catch {
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
}
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutCompleted(event.data.object)
break
}
return NextResponse.json({ received: true })
}
L'idempotence est critique : Stripe peut renvoyer le même événement plusieurs fois en cas d'échec réseau. Stocker l'event.id en base et vérifier sa présence avant traitement évite les doublons — une précaution valable pour tous les systèmes de webhooks, pas seulement Stripe.
Streaming de réponses avec ReadableStream
Pour les usages IA ou les opérations de longue durée, le streaming évite le timeout des fonctions serverless et améliore l'expérience utilisateur. Le format Server-Sent Events (SSE) est le plus simple à consommer côté client.
// app/api/stream/route.ts
export async function GET() {
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder()
for (let i = 0; i < 5; i++) {
const payload = JSON.stringify({ chunk: i })
controller.enqueue(
encoder.encode('data: ' + payload + '\n\n')
)
await new Promise(resolve => setTimeout(resolve, 200))
}
controller.close()
},
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
})
}
Ce pattern est identique à celui utilisé par la Claude API pour streamer les tokens — voir l'article sur l'intégration Claude API dans Next.js pour un exemple complet avec tool use. Côté client, EventSource natif ou le Vercel AI SDK consomment le flux sans bloquer le thread principal.
Contrôle du cache et revalidation
Les Route Handlers GET sont cachés par défaut dans Next.js si aucun élément dynamique n'est détecté. Deux exports de configuration permettent de contrôler ce comportement explicitement :
// Cache désactivé — données personnalisées ou temps réel
export const dynamic = 'force-dynamic'
// ISR — données publiques semi-stables
export const revalidate = 60
export async function GET() {
const data = await fetch('https://api.example.com/config', {
next: { revalidate: 60 },
})
return NextResponse.json(await data.json())
}
Pour un endpoint retournant des données stables (liste de catégories, configuration publique), le cache représente un gain de performance significatif sans effort supplémentaire. Dès que la réponse dépend d'un cookie ou d'un header utilisateur, Next.js désactive automatiquement le cache — inutile de le préciser explicitement dans ce cas.
Rate limiting avec Upstash Redis
Un endpoint sans rate limiting est vulnérable aux abus et aux coûts incontrôlés — particulièrement pour les endpoints qui appellent une API IA facturée au token. Upstash Redis avec @upstash/ratelimit est la solution la plus adaptée en environnement serverless : pas de connexion persistante, facturation à la requête.
// lib/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
export const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'),
})
// app/api/contact/route.ts
export async function POST(request: NextRequest) {
const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1'
const { success, limit, remaining } = await ratelimit.limit(ip)
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{
status: 429,
headers: {
'X-RateLimit-Limit': String(limit),
'X-RateLimit-Remaining': String(remaining),
},
}
)
}
// Traitement de la requête...
}
Le rate limiting par IP via x-forwarded-for est un bon point de départ. En production, coupler l'IP avec l'identifiant utilisateur authentifié donne une granularité plus fine et évite les faux positifs sur les adresses IP partagées (NAT d'entreprise, proxys mobiles).
Route Handlers vs Server Actions : quand choisir quoi
Les deux mécanismes coexistent dans l'App Router mais couvrent des usages distincts :
| Critère | Route Handler | Server Action |
|---|---|---|
| Consommateur externe (mobile, webhook) | ✅ | ❌ |
| Mutation depuis un formulaire React | Déconseillé | ✅ |
| Streaming SSE ou long-polling | ✅ | Limité |
| Typage TypeScript end-to-end automatique | Nécessite tRPC | ✅ |
| Cache Next.js natif sur GET | ✅ | Non applicable |
La règle pratique : si le consommateur est un composant React du même projet, les Server Actions sont plus simples et mieux typées. Si le consommateur est externe ou si vous avez besoin d'un contrat HTTP explicite (webhook, API publique, SDK mobile), les Route Handlers sont le bon choix.
En pratique
Sur les projets Kreio, les Route Handlers couvrent trois cas récurrents : les webhooks de paiement Stripe, les endpoints consommés par des apps React Native, et les flux SSE pour les fonctionnalités IA. Pour les mutations depuis l'interface React — création, mise à jour, suppression — les Server Actions Next.js restent le premier choix, plus simples et sans sérialisation manuelle. Le middleware App Router complète ce dispositif pour l'authentification et le rate limiting global, avant même que la requête n'atteigne le Route Handler.
Chez Kreio, agence Next.js basée à Évreux (Normandie), on applique ces patterns sur des projets clients en production. Besoin d'un audit ou d'un renfort tech ? Parlons-en.