Tous les articles
·Ingénierie·11 min

Intégrer Claude API dans Next.js App Router : streaming, tool use et RAG

Intégrez Claude API dans Next.js App Router : streaming, tool use et RAG avec pgvector. Patterns production testés pour intégrer l'IA dans vos apps.

L'IA générative est passée du prototype à la production. Intégrer Claude API dans une application Next.js App Router soulève des questions concrètes : comment streamer les réponses sans bloquer l'UI, structurer les appels d'outils (tool use) côté serveur, et organiser un pipeline RAG sans transformer le projet en spaghetti asynchrone.

Cet article détaille les patterns qui fonctionnent en production, pas les démos, les vrais. On suppose que vous connaissez l'App Router et TypeScript strict ; l'objectif est d'aller directement au code utile.

Interface d'une application Next.js intégrant un assistant IA avec streaming en temps réel
Streaming de réponses Claude dans une app Next.js App Router

Pourquoi utiliser Claude API directement plutôt qu'un SDK IA générique

Il existe des abstractions comme Vercel AI SDK ou LangChain qui wrappent plusieurs fournisseurs. Elles ont leur place pour tester rapidement, mais en production elles introduisent une couche opaque entre votre code et le comportement réel du modèle. Chaque version majeure casse quelque chose, les types TypeScript sont souvent approximatifs, et vous perdez l'accès direct aux fonctionnalités avancées.

Claude API expose des primitives puissantes directement exploitables : streaming natif par Server-Sent Events, tool use structuré, vision multi-images, et prompt caching. Utiliser le SDK Anthropic TypeScript directement vous donne accès à ces fonctionnalités sans indirection.

// npm install @anthropic-ai/sdk

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

// Singleton pattern : une seule instance pour toute l'application
// Évite de créer N connexions HTTP inutiles à chaque requête
export const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY!, // Jamais exposé côté client
});

Le ANTHROPIC_API_KEY doit rester exclusivement côté serveur. Dans Next.js, les variables d'environnement sans préfixe NEXT_PUBLIC_ ne sont jamais bundlées dans le client, c'est votre premier niveau de sécurité, sans configuration supplémentaire.

Streamer une réponse dans un Route Handler Next.js

Le streaming est la différence entre une UX perçue comme réactive et une UX perçue comme cassée. Personne ne veut fixer un spinner pendant 8 secondes sur une réponse IA. Next.js supporte le streaming natif via l'API Web Streams standard dans les Route Handlers.

// app/api/chat/route.ts
import { anthropic } from '@/lib/anthropic';
import { NextRequest } from 'next/server';

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

  // Ouvrir un stream côté Anthropic : la réponse arrive par delta de texte
  const stream = await anthropic.messages.stream({
    model: 'claude-sonnet-4-5',
    max_tokens: 1024,
    messages,
  });

  // Transformer le stream Anthropic en ReadableStream Web standard
  // C'est ce que Next.js envoie au navigateur via SSE
  const readable = new ReadableStream({
    async start(controller) {
      for await (const chunk of stream) {
        if (
          chunk.type === 'content_block_delta' &&
          chunk.delta.type === 'text_delta'
        ) {
          controller.enqueue(new TextEncoder().encode(chunk.delta.text));
        }
      }
      controller.close();
    },
  });

  return new Response(readable, {
    headers: {
      'Content-Type': 'text/plain; charset=utf-8',
      // Désactiver le buffering Nginx/Vercel : sans ça, les chunks sont batché
      'X-Accel-Buffering': 'no',
      'Cache-Control': 'no-cache',
    },
  });
}

Côté client, un hook minimaliste suffit pour consommer le stream et mettre à jour l'état React au fil des chunks reçus, sans dépendance externe. L'event loop JavaScript traite chaque chunk dès qu'il arrive, donnant cette impression de frappe en direct caractéristique des interfaces IA modernes.

Tool use : faire appeler des fonctions à Claude

Le tool use permet à Claude de décider d'appeler des fonctions que vous définissez. C'est fondamentalement différent du JSON mode ou du prompt engineering classique, le modèle comprend sémantiquement quand et comment utiliser chaque outil, et peut en enchaîner plusieurs avant de produire sa réponse finale.

// lib/tools.ts
import { Tool } from '@anthropic-ai/sdk/resources';

export const tools: Tool[] = [
  {
    name: 'get_product_info',
    // La description est ce que Claude lit pour décider d'utiliser l'outil
    // Précisez le "quand", pas seulement le "quoi"
    description:
      "Récupère les informations d'un produit par son ID. À appeler quand l'utilisateur demande des détails sur un produit spécifique.",
    input_schema: {
      type: 'object',
      properties: {
        product_id: {
          type: 'string',
          description: "L'identifiant unique du produit (format UUID)",
        },
      },
      required: ['product_id'],
    },
  },
];

La boucle d'exécution doit tourner jusqu'à ce que stop_reason === 'end_turn'. Ne pas gérer cette boucle correctement est la première source de bugs dans les implémentations d'agents :

// lib/agent-loop.ts
export async function runAgentLoop(userMessage: string): Promise<string> {
  const messages: MessageParam[] = [{ role: 'user', content: userMessage }];

  while (true) {
    const response = await anthropic.messages.create({
      model: 'claude-sonnet-4-5',
      max_tokens: 2048,
      tools,
      messages,
    });

    if (response.stop_reason === 'end_turn') {
      return response.content
        .filter((b) => b.type === 'text')
        .map((b) => (b as any).text)
        .join('');
    }

    if (response.stop_reason === 'tool_use') {
      // Ajouter la réponse assistant au contexte (avec les appels d'outils)
      messages.push({ role: 'assistant', content: response.content });

      const toolResults = await Promise.all(
        response.content
          .filter((b) => b.type === 'tool_use')
          .map(async (toolUse) => ({
            type: 'tool_result' as const,
            tool_use_id: (toolUse as any).id,
            content: JSON.stringify(await executeToolCall(toolUse)),
          }))
      );

      // Réinjecter les résultats : Claude les lit pour décider quoi faire ensuite
      messages.push({ role: 'user', content: toolResults });
    }
  }
}

RAG avec Supabase pgvector

Le RAG (Retrieval-Augmented Generation) résout le problème fondamental des LLMs pour les applications métier : Claude ne connaît pas vos données propriétaires. Le principe : avant chaque requête, récupérez les documents les plus pertinents depuis une base vectorielle et injectez-les dans le contexte.

Supabase pgvector offre le meilleur rapport setup/performance pour des volumes inférieurs à 10 millions de vecteurs. Pas de service séparé à gérer, facturation prévisible, et intégration naturelle si vous utilisez déjà Supabase dans votre stack Next.js.

-- Migration Supabase
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE documents (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  content text NOT NULL,
  embedding vector(1536),
  metadata jsonb DEFAULT '{}'
);

-- Index HNSW : recherche approximative mais très rapide pour moins d'un million de documents
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);
// lib/rag.ts : pipeline complet en 3 étapes
export async function ragQuery(userQuestion: string): Promise<string> {
  // 1. Vectoriser la question avec le même modèle que les documents indexés
  const embRes = await anthropic.embeddings.create({
    model: 'voyage-3',
    input: userQuestion,
  });

  // 2. Chercher les K documents les plus similaires par similarité cosinus
  const { data: docs } = await supabase.rpc('match_documents', {
    query_embedding: embRes.embeddings[0].embedding,
    match_count: 5,
    match_threshold: 0.75, // Filtrer les résultats sous ce seuil de pertinence
  });

  const context = (docs ?? []).map((d: any) => d.content).join('\n\n---\n\n');

  // 3. Générer la réponse augmentée des documents récupérés
  const response = await anthropic.messages.create({
    model: 'claude-sonnet-4-5',
    max_tokens: 1024,
    system: `Réponds uniquement à partir des documents fournis. Si la réponse n'y figure pas, dis-le clairement.\n\nDocuments :\n` + context,
    messages: [{ role: 'user', content: userQuestion }],
  });

  return response.content[0].type === 'text' ? response.content[0].text : '';
}

Prompt caching : diviser la facture par 10

Le prompt caching est la fonctionnalité la plus rentable de Claude API pour les applications avec un system prompt long ou des documents de référence stables. Les tokens mis en cache coûtent 10× moins cher que les tokens d'entrée normaux, avec un TTL de 5 minutes renouvelable automatiquement à chaque appel.

// Mettre en cache un system prompt ou des documents volumineux
const response = await anthropic.messages.create({
  model: 'claude-sonnet-4-5',
  max_tokens: 1024,
  system: [
    {
      type: 'text',
      text: 'Tu es un assistant expert en droit des affaires.',
    },
    {
      type: 'text',
      // Documentation de 50k+ tokens : payée plein tarif au 1er appel,
      // puis 10x moins cher pour tous les appels suivants dans la fenêtre de 5 min
      text: largeLegalDocumentation,
      cache_control: { type: 'ephemeral' },
    },
  ],
  messages: [{ role: 'user', content: userQuestion }],
});

// Inspecter les métriques de cache pour mesurer les économies réelles
// Ex : { input_tokens: 150, cache_read_input_tokens: 48000, cache_creation_input_tokens: 0 }
console.log('Usage:', response.usage);

Pour les applications à trafic régulier, le cache se maintient naturellement. Pour un trafic sporadique, une requête de "warm-up" toutes les 4 minutes garantit que le cache reste actif en permanence.

Gérer les erreurs et les rate limits

Claude API suit les codes HTTP standard. Le SDK expose des classes d'erreurs typées, un wrapper de retry générique évite de dupliquer cette logique dans tout le codebase :

// lib/safe-claude.ts
import Anthropic from '@anthropic-ai/sdk';

export async function withRetry<T>(fn: () => Promise<T>, maxAttempts = 3): Promise<T> {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (err instanceof Anthropic.RateLimitError) {
        // 429 : le header Retry-After indique combien de secondes attendre
        const wait = parseInt((err as any).headers?.['retry-after'] ?? '30', 10);
        await new Promise((r) => setTimeout(r, wait * 1000));
        continue;
      }
      if (err instanceof Anthropic.APIStatusError && err.status >= 500) {
        // Erreur serveur transitoire : backoff exponentiel
        await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
        continue;
      }
      throw err; // Erreurs client (400, 401, 422) : ne pas retenter
    }
  }
  throw new Error(`Claude call failed after ${maxAttempts} attempts`);
}

Loguez systématiquement response.usage, c'est votre seule source de vérité pour suivre les coûts et détecter les anomalies, notamment les context windows qui gonflent progressivement dans des boucles agents mal bornées.

En pratique

La stack minimale viable tient en 4 fichiers côté serveur : un singleton client, une définition d'outils, un Route Handler streamé, et un utilitaire RAG. Aucune dépendance framework IA tiers n'est nécessaire pour couvrir les cas d'usage courants.

Pour aller plus loin sur les patterns côté serveur, les Server Actions Next.js permettent d'invoquer des appels Claude directement depuis des formulaires React sans Route Handler intermédiaire. Si vous utilisez Supabase pour stocker vos vecteurs et données applicatives, Drizzle vs Prisma vous guidera vers l'ORM adapté à votre volume et vos patterns de requêtes.

Claude API + Next.js App Router est aujourd'hui la combinaison la plus productive pour mettre de l'IA en production rapidement. Streaming natif, tool use structuré et prompt caching couvrent 95 % des cas d'usage sans abstraction tierce. Chez Kreio, agence Next.js basée à Évreux (Normandie), on applique ces patterns sur des projets clients en production. Besoin d'un audit de votre architecture IA ou d'un renfort tech ? Parlons-en.

Sources