Aller au contenu principal
IA Générative & Agents24 juillet 202611 min de lecture

Intégrer un LLM dans une app Next.js : guide pas à pas avec Claude et GPT

Branchez un modèle Claude (Anthropic) ou GPT (OpenAI) dans une app Next.js 16 : Route Handler côté serveur, streaming token par token dans un chat React 19, sortie structurée JSON et garde-fous de production (erreurs, coûts, rate limits).

Par Thomas Dubreuil

Intégrer un LLM dans une app Next.js : guide pas à pas avec Claude et GPT
Sommaire(10 sections)

Votre app Next.js tourne, et on vous demande d'y ajouter une fonctionnalité qui « utilise l'IA » : un chat de support, un résumé automatique, un classement des messages entrants. La vraie question n'est pas de savoir si c'est possible, mais où brancher le modèle sans exposer votre clé d'API dans le navigateur, et comment afficher une réponse qui s'écrit mot après mot plutôt qu'un long temps d'attente suivi d'un pavé.

Ce guide construit exactement ça, pas à pas. Vous allez créer un Route Handler côté serveur dans une app Next.js 16, appeler un modèle Claude (Anthropic) puis un modèle GPT (OpenAI) en streaming, afficher la réponse token par token dans un composant React 19 (le chat), puis récupérer une sortie structurée en JSON typé pour des fonctionnalités non conversationnelles (classer, extraire, résumer). On termine par les garde-fous de production : erreurs, coûts et rate limits.

Un avertissement d'entrée : les identifiants de modèles et les versions de SDK évoluent vite. Les valeurs citées ici (claude-opus-4-8, gpt-5.5, l'API Responses d'OpenAI) sont celles documentées par chaque éditeur au moment de la rédaction. Avant un déploiement réel, revérifiez-les contre la documentation officielle, dont les liens sont réunis en fin d'article.

Prérequis

Ce tutoriel vise un développeur déjà à l'aise avec React et Next.js. Concrètement, mieux vaut avoir en tête :

  • Les bases de Next.js 16 (App Router, Server Components) et la distinction composant serveur / composant client.
  • Node.js 20 ou plus, et un gestionnaire de paquets (pnpm, npm ou yarn).
  • Une clé API Anthropic et une clé API OpenAI, placées dans un fichier .env.local jamais commité.
  • Quelques notions de LLM, de tokens et de streaming (un rappel est possible via le glossaire lié en fin d'article).

Étape 1 : préparer le projet, les clés et les SDK

But. Installer les deux SDK officiels, déclarer les clés d'API, et s'assurer qu'elles ne fuient jamais vers le navigateur.

Depuis la racine de votre projet Next.js, installez les deux clients :

pnpm add @anthropic-ai/sdk openai

Créez (ou complétez) un fichier .env.local à la racine. Ce fichier ne doit jamais partir dans Git : ajoutez-le à votre .gitignore.

# .env.local
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...

Point crucial de sécurité : dans Next.js, toute variable d'environnement sans le préfixe NEXT_PUBLIC_ reste strictement côté serveur. Elle n'est jamais incluse dans le bundle JavaScript envoyé au navigateur. C'est pour cette raison qu'appeler directement le SDK depuis un composant « use client » exposerait votre clé dans le code livré au navigateur. L'appel doit donc partir d'un endroit qui ne s'exécute que côté serveur : un Server Component, une Server Action, ou, le choix de ce guide, un Route Handler.

Avant d'écrire du code, un panorama de ce qu'un LLM branché à votre app permet de construire : un chat de support conversationnel (en streaming), de la génération de contenu, l'extraction et la structuration de données à partir de texte libre, la classification ou la modération de messages entrants, et le résumé de documents ou de conversations. Les premiers exemples s'appuient sur le streaming, que l'on voit tout de suite ; les suivants sur un appel unique en sortie structurée, que l'on couvre à l'étape 5.

Étape 2 : créer le Route Handler Next.js 16

But. Poser le squelette de l'endpoint serveur qui recevra les messages du chat et parlera aux modèles.

Dans l'App Router, un Route Handler se déclare dans un fichier route.ts sous app/. Il exporte une fonction nommée par verbe HTTP. Créez app/api/chat/route.ts :

// app/api/chat/route.ts
export async function POST(request: Request) {
  const { messages } = await request.json();

  // messages est un tableau { role, content } envoyé par le client.
  // On renverra un vrai flux à l'étape 4.
  return new Response("À venir");
}

Un Route Handler utilise les API Web natives Request et Response, sans framework HTTP additionnel. Un verbe non défini renvoie automatiquement un 405 Method Not Allowed. Surtout, il s'exécute uniquement côté serveur, dans le runtime Node par défaut : c'est la frontière naturelle entre le navigateur (qui ne doit jamais connaître vos clés) et l'API du fournisseur. La convention de fichier route, le fichier spécial qui transforme un segment en endpoint, est résumée par ce schéma de la documentation Next.js.

Diagramme de la documentation Next.js montrant la convention de fichier route.js ou route.ts qui transforme un segment de l'App Router en Route Handler côté serveur

Étape 3 : appeler Claude puis GPT en streaming

But. Envoyer les messages à chaque modèle et récupérer sa réponse fragment par fragment. C'est ici que les deux SDK divergent.

Côté Anthropic, on instancie le client (qui lit ANTHROPIC_API_KEY dans l'environnement) et on ouvre un flux avec messages.stream. On itère les événements avec for await et on ne garde que les deltas de texte. Le modèle Claude par défaut recommandé ici est claude-opus-4-8 ; pour un premier essai plus économique, claude-haiku-4-5 fait très bien l'affaire.

import Anthropic from "@anthropic-ai/sdk";

const anthropic = new Anthropic();

const stream = anthropic.messages.stream({
  model: "claude-opus-4-8",
  max_tokens: 1024,
  messages: [{ role: "user", content: userMessage }],
});

for await (const event of stream) {
  if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
    process.stdout.write(event.delta.text);
  }
}

Côté OpenAI, la documentation recommande désormais l'API Responses (responses.create) comme point d'entrée par défaut, plutôt que l'ancienne API Chat Completions, qui reste supportée et utile si vous maintenez du code déjà écrit dessus. Deux différences de forme sautent aux yeux : le paramètre d'entrée s'appelle input (et non messages), et l'événement de delta texte se nomme response.output_text.delta. Le modèle GPT phare est désormais GPT-5.6 (palier Sol) ; les exemples ci-dessous utilisent encore gpt-5.5, un identifiant toujours valide, à remplacer au besoin par la variante GPT-5.6 de votre choix. Pour un premier essai économique, gpt-5.4-mini suffit.

import OpenAI from "openai";

const openai = new OpenAI();

const stream = await openai.responses.create({
  model: "gpt-5.5",
  input: [{ role: "user", content: userMessage }],
  stream: true,
});

for await (const event of stream) {
  if (event.type === "response.output_text.delta") {
    process.stdout.write(event.delta);
  }
}

Même idée des deux côtés : passer une liste de messages, activer le streaming, itérer un flux d'événements asynchrone. Mais les noms changent à chaque étage : messages contre input, content_block_delta avec text_delta contre response.output_text.delta. Retenir cette différence de forme est le vrai enseignement de l'étape : deux fournisseurs, une même mécanique de streaming, deux vocabulaires distincts.

Étape 4 : streamer la réponse vers le client et l'afficher en React 19

But. Transformer le flux du modèle en réponse HTTP streamée, puis la consommer dans un composant client qui affiche le texte au fil de l'eau.

Next.js sait renvoyer un flux progressif depuis un Route Handler. C'est le même principe que le streaming de rendu illustré ci-dessous, appliqué ici à un flux de texte brut que l'on pilote nous-mêmes.

Diagramme de la documentation Next.js illustrant le streaming, où le serveur envoie la réponse en plusieurs morceaux successifs au lieu d'attendre le rendu complet

Concrètement, on branche le flux du SDK sur un ReadableStream de l'API Web Streams. À chaque delta, on encode le texte et on l'empile dans le flux ; à la fin, on ferme le contrôleur. Voici le Route Handler complet côté Claude :

// app/api/chat/route.ts
import Anthropic from "@anthropic-ai/sdk";

const anthropic = new Anthropic();

export async function POST(request: Request) {
  const { messages } = await request.json();
  const userMessage = messages[messages.length - 1].content;

  const modelStream = anthropic.messages.stream({
    model: "claude-opus-4-8",
    max_tokens: 1024,
    messages: [{ role: "user", content: userMessage }],
  });

  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    async start(controller) {
      for await (const event of modelStream) {
        if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
          controller.enqueue(encoder.encode(event.delta.text));
        }
      }
      controller.close();
    },
  });

  return new Response(stream, {
    headers: { "Content-Type": "text/plain; charset=utf-8" },
  });
}

Le même patron marche côté OpenAI : on remplace la boucle par le flux Responses et on filtre response.output_text.delta pour appeler controller.enqueue(encoder.encode(event.delta)).

Côté navigateur, un composant client lit ce flux avec l'API Web Streams standard. Rien de spécifique à React 19 n'est requis ici : ni use(), ni Suspense, qui servent au flux de rendu, pas à un flux de données lu à la main. La seule directive nécessaire est « use client », et un simple useState suffit à accumuler le texte affiché.

"use client";

import { useState } from "react";

export function Chat() {
  const [input, setInput] = useState("");
  const [answer, setAnswer] = useState("");

  async function sendMessage() {
    setAnswer("");
    const response = await fetch("/api/chat", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ messages: [{ role: "user", content: input }] }),
    });

    const reader = response.body!.getReader();
    const decoder = new TextDecoder();

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      setAnswer((prev) => prev + decoder.decode(value));
    }
  }

  return (
    <div>
      <textarea value={input} onChange={(e) => setInput(e.target.value)} />
      <button onClick={sendMessage}>Envoyer</button>
      <p>{answer}</p>
    </div>
  );
}

Résultat attendu. En tapant une question puis en cliquant sur Envoyer, la réponse s'affiche progressivement, mot après mot, au lieu d'apparaître d'un bloc après plusieurs secondes. Vous avez un chat fonctionnel, du navigateur jusqu'au modèle.

Une limite à connaître : ce chat natif est mono-tour. Le Route Handler ne transmet au modèle que le dernier message (messages[messages.length - 1].content), sans l'historique de la conversation : chaque question est traitée isolément, sans mémoire des échanges précédents. C'est suffisant pour un assistant question-réponse, mais une vraie conversation à plusieurs tours suppose de transmettre tout le tableau messages au modèle. La version Vercel AI SDK de l'étape 7 gère cette mémoire multi-tour à votre place via convertToModelMessages(messages).

Étape 5 : au-delà du chat, l'appel one-shot en sortie structurée (JSON)

But. Pour les fonctionnalités non conversationnelles, obtenir directement du JSON typé et validé, sans parser du texte à la main.

Le chat est la démo la plus parlante, mais en production le pattern le plus réutilisé est souvent un appel unique, non streamé, qui renvoie une structure : classer un message entrant, extraire des champs d'un texte libre, résumer. Les deux éditeurs proposent aujourd'hui une sortie structurée validée par Zod. On part d'un même schéma :

import { z } from "zod";

const Triage = z.object({
  categorie: z.enum(["support", "vente", "spam"]),
  urgence: z.enum(["faible", "moyenne", "haute"]),
});

Côté Claude, on appelle messages.parse avec le schéma passé à output_config.format, puis on lit parsed_output :

import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";

const response = await anthropic.messages.parse({
  model: "claude-opus-4-8",
  max_tokens: 1024,
  messages: [{ role: "user", content: `Classe ce message : ${message}` }],
  output_config: { format: zodOutputFormat(Triage) },
});

const triage = response.parsed_output;
// { categorie: "support", urgence: "haute" }

Côté OpenAI, la méthode s'appelle responses.parse, le format passe par text.format, et la sortie se lit dans output_parsed :

import { zodTextFormat } from "openai/helpers/zod";

const response = await openai.responses.parse({
  model: "gpt-5.5",
  input: [{ role: "user", content: `Classe ce message : ${message}` }],
  text: { format: zodTextFormat(Triage, "triage") },
});

const triage = response.output_parsed;
// { categorie: "support", urgence: "haute" }

Même bibliothèque de validation (Zod), même schéma source, mais des noms qui diffèrent : messages.parse contre responses.parse, output_config.format avec zodOutputFormat contre text.format avec zodTextFormat, parsed_output contre output_parsed. L'appel .parse valide automatiquement la réponse contre votre schéma : vous récupérez un objet typé, prêt à l'emploi, plutôt qu'une chaîne à découper. La sortie structurée n'est donc pas une astuce maison, c'est un pattern devenu standard chez les deux éditeurs.

Étape 6 : gérer les erreurs, les coûts et les rate limits

But. Ne pas passer en production sans garde-fous sur les 429, les pannes réseau et la dépense de tokens.

Premier réflexe, souvent oublié : votre endpoint /api/chat est public. Tel quel, n'importe qui peut l'appeler en boucle et faire grimper votre facture de tokens. Avant la mise en production, protégez la route (authentification et rate-limiting applicatif, en plus des limites du fournisseur) et validez le corps de la requête : messages[messages.length - 1].content lève une exception si messages est vide ou mal formé. Rejetez tôt une entrée invalide avec un code 400, avant même d'appeler le modèle.

Première bonne nouvelle : les deux SDK réessaient déjà automatiquement les erreurs transitoires (429 et 5xx) avec un backoff exponentiel, jusqu'à 2 fois par défaut. Vous ajustez ce comportement à la construction du client :

const anthropic = new Anthropic({ maxRetries: 3 });
const openai = new OpenAI({ maxRetries: 3, timeout: 60_000 });

Pour tout ce que le retry automatique ne couvre pas, capturez les exceptions typées du plus spécifique au plus général, sans jamais un catch générique qui masquerait la distinction entre une erreur qu'on peut réessayer (429) et une qui ne se réessaie pas (clé invalide) :

import Anthropic from "@anthropic-ai/sdk";

try {
  const response = await anthropic.messages.parse({ /* ... */ });
} catch (error) {
  if (error instanceof Anthropic.RateLimitError) {
    // 429 : temporiser, puis r&eacute;essayer
  } else if (error instanceof Anthropic.AuthenticationError) {
    // 401 : cl&eacute; absente ou invalide, inutile de r&eacute;essayer
  } else if (error instanceof Anthropic.APIError) {
    // autre erreur renvoy&eacute;e par l'API
  }
}

Côté coûts, deux réflexes. D'abord, toujours borner la sortie : max_tokens est obligatoire chez Anthropic, et max_output_tokens joue le même rôle côté OpenAI. Un appel sans plafond est un appel dont on ne maîtrise pas la facture. Ensuite, choisir le modèle le moins cher qui suffit à la tâche : un Haiku ou un gpt-5.4-mini pour de la classification simple ou du prototypage, un Opus ou un GPT-5.6 (palier Sol) réservé au raisonnement complexe et au code. Sur les modèles récents, le niveau d'effort de raisonnement se règle aussi (output_config.effort côté Claude, reasoning.effort côté OpenAI) : un effort plus bas coûte moins cher sur les tâches courtes.

Dernier garde-fou : avant de faire confiance à une réponse, vérifiez qu'elle n'a pas été tronquée par le plafond de tokens (le stop_reason côté Claude). Une réponse coupée n'est pas une réponse complète. Et journalisez l'usage renvoyé par chaque appel (input_tokens et output_tokens) pour surveiller la dépense réelle.

Étape 7 : aller plus loin, unifier les fournisseurs

But. Cacher les différences entre SDK derrière une interface unique, et ouvrir sur les briques suivantes.

Vous avez vu les deux SDK natifs côte à côte, avec leurs vocabulaires distincts. Le Vercel AI SDK propose une couche d'abstraction : une même fonction streamText où seul l'objet model change pour basculer d'un fournisseur à l'autre. On installe le cœur et les providers :

pnpm add ai @ai-sdk/anthropic @ai-sdk/openai @ai-sdk/react zod

Le Route Handler devient beaucoup plus court, et la méthode toUIMessageStreamResponse produit directement une réponse prête pour un chat :

// app/api/chat/route.ts
import { anthropic } from "@ai-sdk/anthropic";
import { streamText, convertToModelMessages } from "ai";

export async function POST(request: Request) {
  const { messages } = await request.json();

  const result = streamText({
    model: anthropic("claude-opus-4-8"),
    messages: convertToModelMessages(messages),
  });

  return result.toUIMessageStreamResponse();
}

Pour passer de Claude à GPT, une seule ligne change : model: openai("gpt-5.5"), après un import { openai } from "@ai-sdk/openai". Côté client, le hook useChat gère à votre place l'accumulation du flux et l'affichage token par token, ce qui remplace toute la lecture manuelle du ReadableStream de l'étape 4 :

"use client";

import { useChat } from "@ai-sdk/react";

export function Chat() {
  const { messages, sendMessage } = useChat();
  // messages contient l'historique ; sendMessage envoie un tour.
}

À partir de ce socle, trois directions élargissent l'assistant. Le tool use (ou function calling) laisse Claude et GPT appeler des fonctions de votre application. Le RAG connecte le modèle aux données de votre entreprise. Enfin, le MCP standardise la connexion des IA à des outils et des données externes. Chacun de ces sujets est couvert par un article dédié, réuni dans la section suivante.

Pour aller plus loin

Sources officielles

La documentation primaire de chaque techno, à revérifier avant un déploiement car les identifiants de modèles et les SDK évoluent vite :


Thomas Dubreuil

Thomas Dubreuil

Lead développeur

Pour aller plus loin

LLM open source en 2026 : l'écart s'est resserré, le choix se joue ailleurs
IA Générative & Agents22 juillet 20269 min

LLM open source en 2026 : l'écart s'est resserré, le choix se joue ailleurs

Panorama des meilleurs LLM open source (à poids ouverts) en 2026 : gpt-oss, Mistral, Qwen, DeepSeek, GLM, Gemma. L'écart avec les modèles fermés se resserre ; pour une équipe, le choix se joue sur la licence, le coût d'hébergement et la conformité, pas sur le seul score.

Eliott Bidault-HervouetEliott Bidault-Hervouet
Sakana AI lance Fugu, le système multi-modèle qui apprend à coordonner ses IA
IA Générative & Agents9 juillet 20263 min

Sakana AI lance Fugu, le système multi-modèle qui apprend à coordonner ses IA

Fugu expose un groupe d'agents spécialisés comme un seul modèle via une API compatible OpenAI. Premier bilan de ce laboratoire tokyoïte qui parie sur la coordination apprise plutôt que codée.

Pierre GouedarPierre Gouedar
Le glossaire de l’IA générative : 27 mots à connaître, expliqués simplement
IA Générative & Agents6 juillet 202613 min

Le glossaire de l’IA générative : 27 mots à connaître, expliqués simplement

Un glossaire de référence pour comprendre le vocabulaire de l’IA générative en 2026 : 27 termes, de LLM à MCP, définis en deux ou trois phrases claires, sans jargon.

Pierre GouedarPierre Gouedar

Réservez un rendez-vous gratuit avec un spécialiste

30 minutes pour échanger sur votre projet digital et vos enjeux tech.

Équipe Koul en train de brainstormer
Questions fréquentes

Le blog Koul

Ligne éditoriale, sources, usage : ce qui sort sur ce blog et comment vous pouvez vous en servir.