?
TemplateintermédiaireVérifié le 2025-05

Template : app Next.js + OpenAI streaming

Boilerplate pour une application Next.js avec streaming OpenAI et gestion d'erreurs.

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 utilisateur

Endpoint 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).

Sources

APINext.jsstreaming