Comment intégrer l'IA Claude dans une app Next.js
Retour d'expérience sur l'intégration de l'API Claude dans un projet Next.js en production — streaming, gestion d'erreurs et les pièges que j'ai rencontrés.
Sur un projet client récent, on devait intégrer un assistant IA dans une app Next.js existante. Après avoir testé plusieurs APIs (OpenAI, Mistral, Claude), j'ai gardé Claude pour un truc tout bête : la qualité des réponses en contexte long et le streaming qui juste marche. Voici ce que j'ai appris en faisant tourner ça en production.
Pourquoi Claude pour votre application Next.js ?#
Après avoir itéré sur plusieurs modèles en production, voici ce qui fait la différence au quotidien :
- Fenêtre de contexte étendue : jusqu'à 200K tokens, idéal pour analyser de longs documents
- Qualité de raisonnement : excellent pour les tâches complexes nécessitant une réflexion structurée
- API bien conçue : SDK officiel TypeScript avec un typage complet
- Streaming natif : réponses en temps réel pour une UX fluide
Ce tutoriel utilise Next.js 14+ avec l'App Router. Les concepts s'appliquent aussi aux versions antérieures avec quelques adaptations au niveau du routing.
Prérequis et installation#
Commençons par installer le SDK officiel d'Anthropic :
npm install @anthropic-ai/sdkAjoutez votre clé API dans .env.local :
ANTHROPIC_API_KEY=sk-ant-api03-votre-cle-iciNe commitez jamais votre clé API. Assurez-vous que .env.local est bien dans votre .gitignore.
Créer une Route API avec streaming#
Le cœur de l'intégration repose sur une Route API Next.js qui communique avec Claude et retourne la réponse en streaming. Cette approche garantit une expérience utilisateur réactive.
La route handler#
Créez le fichier app/api/chat/route.ts :
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
export async function POST(request: Request) {
const { message, conversationHistory } = await request.json();
if (!message || typeof message !== "string") {
return Response.json(
{ error: "Le message est requis" },
{ status: 400 }
);
}
const messages = [
...(conversationHistory || []),
{ role: "user" as const, content: message },
];
const stream = client.messages.stream({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
system:
"Tu es un assistant technique expert en développement web. " +
"Tu réponds en français de manière concise et précise.",
messages,
});
const encoder = new TextEncoder();
const readableStream = new ReadableStream({
async start(controller) {
for await (const event of stream) {
if (
event.type === "content_block_delta" &&
event.delta.type === "text_delta"
) {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ text: event.delta.text })}\n\n`)
);
}
}
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.close();
},
});
return new Response(readableStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}Points clés de l'implémentation#
Plusieurs décisions techniques méritent une explication :
client.messages.stream()plutôt queclient.messages.create(): le streaming permet d'afficher les tokens au fur et à mesure, réduisant le temps perçu de réponse- Server-Sent Events (SSE) : le format
data: ...\n\nest standard pour le streaming côté navigateur - Historique de conversation : on transmet l'historique complet pour que Claude maintienne le contexte
Le composant client React#
Côté frontend, nous avons besoin d'un composant qui envoie les messages et consomme le stream :
"use client";
import { useState, useRef, useCallback } from "react";
interface Message {
role: "user" | "assistant";
content: string;
}
export default function Chat() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [isStreaming, setIsStreaming] = useState(false);
const abortRef = useRef<AbortController | null>(null);
const sendMessage = useCallback(async () => {
if (!input.trim() || isStreaming) return;
const userMessage = input.trim();
setInput("");
const updatedMessages: Message[] = [
...messages,
{ role: "user", content: userMessage },
];
setMessages(updatedMessages);
setIsStreaming(true);
abortRef.current = new AbortController();
try {
const response = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: userMessage,
conversationHistory: messages,
}),
signal: abortRef.current.signal,
});
if (!response.ok) throw new Error("Erreur API");
if (!response.body) throw new Error("Pas de body");
const reader = response.body.getReader();
const decoder = new TextDecoder();
let assistantContent = "";
setMessages((prev) => [
...prev,
{ role: "assistant", content: "" },
]);
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split("\n");
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const data = line.slice(6);
if (data === "[DONE]") break;
const parsed = JSON.parse(data);
assistantContent += parsed.text;
setMessages((prev) => {
const updated = [...prev];
updated[updated.length - 1] = {
role: "assistant",
content: assistantContent,
};
return updated;
});
}
}
} catch (error) {
if (error instanceof Error && error.name !== "AbortError") {
setMessages((prev) => [
...prev,
{
role: "assistant",
content: "Une erreur est survenue. Réessayez.",
},
]);
}
} finally {
setIsStreaming(false);
}
}, [input, isStreaming, messages]);
return (
<div className="flex flex-col h-[600px]">
<div className="flex-1 overflow-y-auto space-y-4 p-4">
{messages.map((msg, i) => (
<div
key={i}
className={
msg.role === "user" ? "text-right" : "text-left"
}
>
<p className="inline-block px-4 py-2 rounded-lg max-w-[80%]">
{msg.content}
</p>
</div>
))}
</div>
<div className="flex gap-2 p-4 border-t">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && sendMessage()}
placeholder="Votre message..."
className="flex-1 px-4 py-2 rounded-lg border"
disabled={isStreaming}
/>
<button
onClick={sendMessage}
disabled={isStreaming}
className="px-6 py-2 rounded-lg bg-purple-600 text-white"
>
Envoyer
</button>
</div>
</div>
);
}Gestion des erreurs et rate limiting#
En production, vous devez gérer plusieurs scénarios d'erreur. Voici un wrapper robuste :
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
const RATE_LIMIT_WINDOW = 60_000;
const MAX_REQUESTS_PER_WINDOW = 20;
const requestLog: number[] = [];
function checkRateLimit(): boolean {
const now = Date.now();
const windowStart = now - RATE_LIMIT_WINDOW;
while (requestLog.length > 0 && requestLog[0] < windowStart) {
requestLog.shift();
}
if (requestLog.length >= MAX_REQUESTS_PER_WINDOW) return false;
requestLog.push(now);
return true;
}
export async function callClaude(
messages: Anthropic.MessageParam[],
options?: { maxTokens?: number; temperature?: number }
) {
if (!checkRateLimit()) {
throw new Error("Rate limit dépassé. Réessayez dans un instant.");
}
try {
const response = await client.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: options?.maxTokens ?? 4096,
temperature: options?.temperature ?? 0.7,
messages,
});
return response;
} catch (error) {
if (error instanceof Anthropic.APIError) {
if (error.status === 429) {
throw new Error("Trop de requêtes — réessayez dans quelques secondes.");
}
if (error.status === 529) {
throw new Error("API surchargée — réessayez plus tard.");
}
throw new Error(`Erreur API : ${error.message}`);
}
throw error;
}
}En production, utilisez un vrai rate limiter comme @upstash/ratelimit avec Redis plutôt qu'un tableau en mémoire. Le rate limit en mémoire ne survit pas aux redéploiements et ne fonctionne pas en serverless.
Optimisation des performances#
Mise en cache des réponses#
Pour les requêtes identiques, vous pouvez mettre en cache les réponses afin de réduire les coûts et la latence :
import { unstable_cache } from "next/cache";
const getCachedResponse = unstable_cache(
async (prompt: string) => {
const response = await client.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
messages: [{ role: "user", content: prompt }],
});
return response.content[0].type === "text"
? response.content[0].text
: "";
},
["claude-response"],
{ revalidate: 3600 }
);Gestion du contexte#
Claude excelle quand le contexte est bien structuré. Voici une approche pour optimiser vos prompts :
function buildSystemPrompt(context: {
userRole: string;
appSection: string;
}): string {
return [
`L'utilisateur est un ${context.userRole}.`,
`Il se trouve dans la section "${context.appSection}" de l'application.`,
"Adapte tes réponses à son niveau technique et au contexte.",
"Sois concis. Utilise des exemples de code quand c'est pertinent.",
].join(" ");
}Déploiement sur Vercel#
L'intégration fonctionne nativement sur Vercel. Quelques points d'attention :
- Variables d'environnement : ajoutez
ANTHROPIC_API_KEYdans les settings du projet Vercel - Timeout des fonctions : le plan Hobby limite à 10 secondes — le streaming contourne cette limitation car la connexion reste active
- Edge Runtime : si vous avez besoin de temps de réponse plus courts, vous pouvez utiliser l'Edge Runtime
export const runtime = "edge";L'Edge Runtime ne supporte pas toutes les fonctionnalités Node.js. Testez bien votre intégration avant de migrer.
Conclusion#
Intégrer Claude dans une application Next.js est étonnamment simple grâce au SDK TypeScript d'Anthropic et aux Route Handlers de Next.js. Les points essentiels à retenir :
- Utilisez le streaming pour une UX réactive
- Gérez les erreurs de manière explicite (rate limits, timeouts, erreurs API)
- Structurez vos prompts avec un system prompt contextuel
- Cachez les réponses quand c'est possible pour réduire les coûts
- Sécurisez votre clé API côté serveur uniquement
L'IA n'est plus un gadget — c'est un outil de production. Et avec les bons patterns, son intégration dans votre stack Next.js est robuste, performante et maintenable.
Besoin d'aide pour intégrer l'IA dans votre application ? Contactez-nous pour discuter de votre projet.
Articles similaires
Créer son propre serveur MCP en TypeScript : le guide complet
J'ai construit plusieurs serveurs MCP en production pour des clients. Voici le guide que j'aurais aimé avoir au départ — de zéro jusqu'au déploiement.
BMAD-METHOD : le framework agile qui transforme le développement avec l'IA
Découvrez BMAD-METHOD, le framework open-source qui structure le développement logiciel piloté par l'IA avec des agents spécialisés et des workflows agiles.
OpenClaw : transformez WhatsApp, Telegram et Discord en interface IA
J'ai installé OpenClaw pour me faire un assistant IA accessible depuis WhatsApp et Telegram. Voici comment ça marche et ce que ça change au quotidien.