Tous les articles
·Ingénierie·8 min

Route Handlers Next.js App Router : webhooks, streaming et bonnes pratiques

Maîtrisez les Route Handlers Next.js App Router : webhooks, streaming, rate limiting et TypeScript strict. Guide complet avec exemples concrets.

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.

Architecture des Route Handlers dans Next.js App Router : fichiers route.ts organisés dans le dossier app/ avec gestion des verbes HTTP
Les Route Handlers se placent dans le dossier app/ aux côtés des pages, mais ne rendent aucun HTML — ils exposent des endpoints HTTP purs.

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èreRoute HandlerServer Action
Consommateur externe (mobile, webhook)
Mutation depuis un formulaire ReactDéconseillé
Streaming SSE ou long-pollingLimité
Typage TypeScript end-to-end automatiqueNécessite tRPC
Cache Next.js natif sur GETNon 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.

Sources