Architecture d'une app Next.js + OpenAI
Ce template présente l'architecture complète pour intégrer le streaming OpenAI dans une application Next.js avec App Router, incluant la gestion d'erreurs, le rate limiting et une UX fluide.
Structure du projet
app/
api/
chat/
route.ts # Endpoint API streaming
chat/
page.tsx # Page principale
lib/
openai.ts # Client OpenAI configuré
rate-limit.ts # Rate limiting
components/
ChatMessage.tsx # Composant message
ChatInput.tsx # Input utilisateurEndpoint API avec streaming
// app/api/chat/route.ts
import OpenAI from "openai";
const openai = new OpenAI();
export async function POST(req: Request) {
try {
const { messages } = await req.json();
const stream = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
{ role: "system", content: "Tu es un assistant utile et concis." },
...messages
],
stream: true
});
const encoder = new TextEncoder();
const readable = new ReadableStream({
async start(controller) {
for await (const chunk of stream) {
const text = chunk.choices[0]?.delta?.content || "";
if (text) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ text })}\n\n`));
}
}
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.close();
}
});
return new Response(readable, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive"
}
});
} catch (error) {
if (error instanceof OpenAI.APIError) {
return Response.json(
{ error: error.message },
{ status: error.status || 500 }
);
}
return Response.json(
{ error: "Erreur interne" },
{ status: 500 }
);
}
}Client React avec streaming
// components/useChat.ts
import { useState, useCallback } from "react";
type Message = { role: "user" | "assistant"; content: string };
export function useChat() {
const [messages, setMessages] = useState<Message[]>([]);
const [isLoading, setIsLoading] = useState(false);
const sendMessage = useCallback(async (content: string) => {
const userMessage: Message = { role: "user", content };
const newMessages = [...messages, userMessage];
setMessages(newMessages);
setIsLoading(true);
try {
const response = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages: newMessages })
});
if (!response.ok) throw new Error("Erreur API");
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let assistantContent = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
for (const line of chunk.split("\n")) {
if (line.startsWith("data: ") && line !== "data: [DONE]") {
const { text } = JSON.parse(line.slice(6));
assistantContent += text;
setMessages([...newMessages, { role: "assistant", content: assistantContent }]);
}
}
}
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
}, [messages]);
return { messages, sendMessage, isLoading };
}Gestion d'erreurs robuste
Erreurs API courantes - 401 : Clé API invalide - 429 : Rate limit atteint (attendre et retry) - 500 : Erreur serveur OpenAI (retry avec backoff) - 503 : Service temporairement indisponible
Retry avec exponential backoff
async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
const delay = Math.pow(2, i) * 1000;
await new Promise(r => setTimeout(r, delay));
}
}
throw new Error("Max retries reached");
}Variables d'environnement
# .env.local
OPENAI_API_KEY=sk-...Ne jamais exposer la clé API au client. Toujours passer par un endpoint serveur (Route Handler).