Les Route Handlers Next.js sont la façon officielle de construire une API dans l'App Router, et pourtant beaucoup d'équipes les traitent encore comme les anciennes API Routes du dossier pages. Résultat : des GET mis en cache sans qu'on l'ait demandé, des réponses 405 inexpliquées et une validation absente qui laisse passer n'importe quel payload.
Un Route Handler, c'est un fichier route.ts qui exporte une fonction par méthode HTTP. Rien de plus en apparence. Mais derrière cette simplicité se cachent des règles de cache, de runtime et de typage qui décident si votre endpoint tient la charge en production ou casse au premier déploiement.
À la fin, vous saurez créer un endpoint typé, valider ses entrées, contrôler son cache et le protéger — sans quitter votre projet Next.js et sans ajouter un serveur Express à côté.
Route Handlers ou API Routes : ce qui change vraiment
La migration depuis les API Routes du dossier pages n'est pas cosmétique. Les anciennes routes reposaient sur les objets req et res de Node, spécifiques à Next.js. Les Route Handlers, eux, s'appuient sur les API web standard Request et Response. Le même code de manipulation de requête fonctionnerait dans un worker Cloudflare ou un edge runtime, ce qui n'était pas le cas avant.
Concrètement, vous n'écrivez plus res.status(200).json(...) mais vous retournez un objet Response. Next.js ajoute par-dessus NextRequest et NextResponse, deux extensions qui apportent des helpers pratiques : lecture des cookies, accès aux paramètres d'URL, redirections typées. Vous gardez la compatibilité avec le standard tout en profitant du confort maison.
Autre différence majeure : un fichier route.ts ne peut pas cohabiter avec un page.tsx au même niveau de segment. Une route sert soit une page, soit une API, jamais les deux. Cette contrainte évite les collisions d'URL et clarifie l'intention de chaque dossier. Pour une équipe qui reprend un projet, savoir qu'un dossier contenant route.ts est forcément une API supprime une source d'ambiguïté quotidienne.
Créer un premier Route Handler avec route.ts
La convention est stricte et c'est une bonne chose : le fichier s'appelle toujours route.ts, il se place n'importe où sous app/, et l'URL découle de l'arborescence. Un fichier dans app/api/articles/route.ts répond sur /api/articles. Vous exportez une fonction nommée d'après la méthode HTTP que vous voulez gérer.
// app/api/articles/route.ts
// On exporte une fonction par méthode : Next.js route automatiquement
// et renvoie un 405 pour toute méthode non déclarée. Pas de switch manuel.
import { NextResponse } from 'next/server'
export async function GET() {
const articles = await db.article.findMany()
return NextResponse.json(articles)
}
Sept méthodes sont supportées : GET, POST, PUT, PATCH, DELETE, HEAD et OPTIONS. Si un client appelle une méthode que vous n'avez pas exportée, Next.js renvoie un 405 Method Not Allowed sans que vous ayez à l'écrire. Ce comportement par défaut est un filet de sécurité : impossible d'exposer accidentellement une mutation via GET.
Ce niveau de convention réduit aussi les revues de code. Tout le monde sait où chercher la logique d'un endpoint et comment il est déclaré, ce qui accélère l'onboarding sur un projet existant.
Gérer les méthodes HTTP et le corps de requête
Lire le corps d'une requête POST se fait avec les méthodes standard de l'objet Request : await request.json() pour du JSON, await request.formData() pour un formulaire multipart. C'est le même contrat que dans un navigateur, ce qui limite les surprises et rend le code testable sans mock Next.js.
// app/api/articles/route.ts
export async function POST(request: Request) {
// On typera la donnée juste après : ici on récupère du non-typé,
// donc on ne fait confiance à rien avant validation.
const body = await request.json()
const created = await db.article.create({ data: body })
return NextResponse.json(created, { status: 201 })
}
Pour les segments dynamiques, le second argument de la fonction expose les paramètres. Dans Next.js 15 et 16, ce params est asynchrone : vous devez l'attendre avec await. Beaucoup de bugs de migration viennent de là, avec des paramètres qui ressortent en Promise non résolue et des undefined inexpliqués. Un console.log sur la valeur brute règle la question en dix secondes.
Le status code se contrôle via le second argument de NextResponse.json. Retourner un 201 sur création, un 404 sur ressource absente et un 422 sur payload invalide n'est pas du détail : vos clients frontend, votre monitoring et vos tests s'appuient dessus.
Valider les entrées avant de toucher la base
Un Route Handler qui insère directement await request.json() en base est une faille ouverte. La donnée entrante n'a aucun type garanti à l'exécution : TypeScript disparaît à la compilation. La règle non négociable sur nos projets : aucune donnée externe ne franchit un Route Handler sans passer par un schéma de validation.
// On valide au bord : si le payload est invalide, on n'atteint jamais la DB.
import { z } from 'zod'
const CreateArticle = z.object({ title: z.string().min(3), slug: z.string() })
export async function POST(request: Request) {
const parsed = CreateArticle.safeParse(await request.json())
if (!parsed.success) {
return NextResponse.json({ errors: parsed.error.flatten() }, { status: 422 })
}
const created = await db.article.create({ data: parsed.data })
return NextResponse.json(created, { status: 201 })
}
Cette approche vous donne deux garanties d'un coup : un rejet propre en 422 avec des messages exploitables côté client, et un objet parsed.data entièrement typé pour la suite du handler. On détaille la mise en place complète dans notre guide sur la validation avec Zod dans Next.js, qui couvre aussi le partage de schémas entre client et serveur.
Cache : pourquoi vos GET ne se comportent pas comme vous croyez
Voici le piège numéro un. Contrairement à une idée répandue, les Route Handlers ne sont plus mis en cache par défaut depuis Next.js 15. Un GET est dynamique par défaut : il s'exécute à chaque requête. Si vous voulez le figer en statique, vous devez l'activer explicitement.
// Sans cette ligne, ce GET s'exécute à chaque appel.
// force-static le transforme en réponse figée au build : parfait pour des
// données rarement modifiées, catastrophique pour un endpoint temps réel.
export const dynamic = 'force-static'
export async function GET() {
return NextResponse.json(await getStaticConfig())
}
Seuls les GET peuvent être mis en cache. POST, PUT, PATCH et DELETE sont toujours dynamiques, ce qui est logique : on ne met pas en cache une mutation. Pour un contrôle plus fin, revalidate permet de rafraîchir la réponse à intervalle régulier plutôt que de choisir entre tout statique et tout dynamique.
Comprendre ce modèle évite des heures de débogage sur des données qui « ne se mettent pas à jour ». Le sujet mérite un article à lui seul, et c'est exactement ce qu'on a fait dans notre analyse du cache Next.js App Router. Lisez-le si un endpoint vous renvoie obstinément d'anciennes valeurs.
Sécuriser et authentifier un Route Handler
Un Route Handler est une porte d'entrée publique. Sans contrôle, n'importe qui atteint votre base. La vérification d'authentification se fait en tête de handler, avant toute logique métier, en lisant le cookie de session ou l'en-tête Authorization via NextRequest.
// La garde d'auth passe avant tout : on coupe court dès la première ligne.
export async function DELETE(request: NextRequest) {
const session = await getSession(request)
if (!session) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 })
}
// ... suppression réservée aux utilisateurs authentifiés
}
Distinguez bien 401 (non authentifié) et 403 (authentifié mais sans les droits). Cette nuance guide le frontend : un 401 déclenche une redirection vers la connexion, un 403 affiche un message d'accès refusé. Pour le choix d'une brique d'authentification adaptée à l'App Router, on compare Clerk, Auth.js et Better Auth dans notre guide d'authentification Next.js.
Ajoutez à cela un rate limiting sur les endpoints sensibles et une validation stricte, et vous couvrez l'essentiel de la surface d'attaque d'une API sur-mesure. La sécurité d'un Route Handler ne se rattrape pas au déploiement : elle se conçoit dès la première ligne du fichier.
Streaming et réponses longues
Parce qu'ils reposent sur l'API Response standard, les Route Handlers savent renvoyer un flux. Au lieu d'attendre qu'une réponse complète soit prête, vous streamez les données au fur et à mesure via un ReadableStream. C'est ce qui permet, par exemple, de relayer token par token la sortie d'un modèle de langage sans faire patienter l'utilisateur devant un écran figé.
Ce cas d'usage est devenu central avec les intégrations d'IA : proxifier l'API d'un LLM derrière un Route Handler garde votre clé secrète côté serveur tout en offrant une expérience fluide côté client. Le même mécanisme sert à exporter un gros fichier CSV ou à pousser des événements en temps réel. Le Route Handler n'est plus un simple endpoint JSON : c'est un point de contrôle capable de gérer des réponses de toute nature, avec les mêmes primitives que le reste de la plateforme web.
En pratique
Les Route Handlers ne sont pas des API Routes déguisées. Ils partagent les primitives web standard avec le reste de votre application Next.js, ce qui rend votre code plus portable et plus testable, mais ils imposent une discipline sur le cache et la validation qu'on ne peut pas ignorer. Un endpoint propre suit toujours le même squelette : garde d'authentification, validation du payload, logique métier, réponse avec le bon status code.
Sur un projet réel, on centralise ces briques. Un helper de réponse d'erreur, un middleware de session, des schémas Zod partagés entre le client et le Route Handler : trois abstractions qui suppriment 80 % du code répété et rendent chaque nouvel endpoint prévisible. C'est ce genre de fondation qu'on pose en début de projet pour que l'ajout d'une route devienne une formalité de dix minutes plutôt qu'une source de régressions. Si vous devez aussi arbitrer entre Route Handlers et Server Actions pour vos mutations, notre article sur les hooks React 19 éclaire le choix côté formulaires.
Le bon réflexe : traiter chaque Route Handler comme une frontière de confiance. Tout ce qui entre est suspect jusqu'à validation, tout ce qui sort porte un status code réfléchi. Adoptez cette hygiène et votre couche API cesse d'être l'endroit où les bugs se cachent.
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.