Voor een Next.js-chatbot die meerdere modelproviders nodig heeft, is de meest bruikbare backend er een die authenticatie centraliseert en de applicatie per request een model laat kiezen. CometAPI biedt dat patroon: de server bewaart één API-sleutel en de OpenAI-compatibele basis-URL https://api.cometapi.com/v1, terwijl het model-veld in het request een beschikbare GPT-, Claude-, Gemini- of ander chatmodel selecteert.
Deze tutorial bouwt een werkend App Router-project met een server-side modellallowlist, een streaming Route Handler, een modelkeuze in de browser, beschermde omgevingsvariabelen, een complete chatinterface en deployrichtlijnen. De CometAPI-sleutel bereikt de browser nooit.
Wat is een multimodel AI-chatapp?
Een multimodel AI-chatarchitectuur plaatst één door de applicatie beheerde backend tussen de gebruikersinterface en meerdere modelproviders. In dit project stuurt de browser het gesprek naar /api/chat, terwijl de server het CometAPI-credential privé houdt, de aangevraagde model-ID valideert en het request via één compatibele API doorstuurt.
Hoe werkt modelswitching in Next.js?
Modellen wisselen is een routeringsbeslissing. De browser stuurt een goedgekeurde model-ID met het gesprek naar /api/chat. De Route Handler voegt het CometAPI-credential toe, roept POST /v1/chat/completions aan en streamt de response van het geselecteerde model terug naar de browser.
Het gedeelde eindpunt maakt niet ieder model identiek. Outputstijl, toolgedrag, ondersteunde parameters, contextlimieten en prijzen kunnen verschillen. Houd model-ID’s in een server-side allowlist en test elke route met dezelfde applicatieprompts voordat je die voor gebruikers beschikbaar maakt.
Wat ga je bouwen
De voltooide applicatie heeft één Next.js-backend en drie selecteerbare chatroutes:
| Model ID | Voorbeeldrol | CometAPI-prijs |
|---|---|---|
| gemini-3.7-flash | Kostenbewuste, high-volume chat | $0.60 invoer / $3.00 uitvoer |
| claude-opus-5 | Gerichte premium reasoning | $4 invoer / $20 uitvoer |
| gpt-5.6 | gpt-5.6 is de generieke CometAPI-route voor GPT-5.6 en mappt momenteel naar de Sol-tier. | $3.2 invoer / $16 uitvoer in de short-context-tier |
Prijsopmerking: De hieronder getoonde prijzen zijn in USD per miljoen tokens, gecontroleerd op 21 augustus 2026, en kunnen veranderen. Verifieer altijd de actuele tarieven op de modelpagina’s voordat je in productie gaat. De tabel is een routeringsvoorbeeld, geen kwaliteitsranglijst. GPT-5.6 gebruikt een hogere prijstier boven 272,000 tokens. Zie de gelinkte modelpagina’s van Gemini 3.7 Flash, Claude Opus 5 en GPT-5.6 voor de dateringen van de hier gebruikte tarieven.
CometAPI prijst modellen transparant ten opzichte van de officiële API van elke provider. Modellen met uniforme officiële prijzen — OpenAI, Claude, Gemini en vergelijkbaar — worden per token gefactureerd tegen een 0.8:1-verhouding tot de officiële prijs, een korting van 20%; modellen zonder officiële API’s (MidJourney, Kling, Luma) worden per call gefactureerd tegen de door CometAPI ingestelde tarieven, eveneens met 20% korting. Zie de CometAPI Pricing Guide voor de opslagformule, per-modeltarieven en facturatie-eenheden.
De drie routes dekken verschillende trade-offs. Gemini 3.7 Flash is Google’s efficiënte agentische werkpaard, met multimodale input en een contextvenster van 1,048,576 tokens voor high-volume chat, coding en kennisworkflows. Claude Opus 5 is Anthropic’s frontier-reasoningmodel, sterk in meerstapsanalyse, code en zorgvuldige long-form schrijfstukken tegen een premiumprijs. GPT-5.6 is OpenAI’s generieke model, dat redeneren, toolgebruik en schrijven balanceert over een breed contextvenster voor dagelijkse productiebelasting.
Voordat je begint
Je hebt Node.js, npm, een CometAPI-account en een server-side API-sleutel nodig uit de CometAPI Quick Start. De drie exacte model-ID’s hierboven waren beschikbaar in de livecatalogus op 21 augustus 2026, waren niet als “upcoming” gemarkeerd en stelden POST /v1/chat/completions bloot.
De tutorial gebruikt deze gedeelde instellingen:
- API-sleutel:
COMETAPI_API_KEY - Basis-URL:
https://api.cometapi.com/v1 - Catalogus-eindpunt:
GEThttps://api.cometapi.com/api/models
Stap 1: Maak de Next.js-app
npx create-next-app@latest multi-model-chat --ts --app --eslintcd multi-model-chatnpm run dev
Voor deze versie is geen provider-SDK vereist. De server gebruikt de ingebouwde fetch-API en zet de upstream Server-Sent Events-stream door.
Stap 2: Houd de CometAPI-sleutel op de server
Maak .env.local in de projectroot:
COMETAPI_API_KEY=replace_with_your_cometapi_keyCOMETAPI_BASE_URL=https://api.cometapi.com/v1
Voorzie de sleutel niet van de prefix NEXT_PUBLIC_. Next.js stelt alleen variabelen met die prefix bloot aan browserbundels; de API-sleutel hoort in de server-side Route Handler.
Stap 3: Definieer het modelbeleid
Zet de toegestane ID’s in de backend, niet alleen in de dropdown. Een gebruiker kan browsercontroles omzeilen en je route rechtstreeks aanroepen, dus de server moet onbekende modelwaarden afwijzen.
const ALLOWED_MODELS = [ "gemini-3.7-flash", "claude-opus-5", "gpt-5.6",] as const;type ModelId = (typeof ALLOWED_MODELS)[number];
De client zal dezelfde drie ID’s gebruiken voor de selector, terwijl de server de bron van waarheid blijft.
Stap 4: Stream CometAPI via een Route Handler
Maak app/api/chat/route.ts. De route valideert een tekst-only chatpayload, roept CometAPI aan met stream: true en retourneert de upstream eventstream zonder het credential bloot te leggen.
export const runtime = "nodejs";export const dynamic = "force-dynamic";const ALLOWED_MODELS = [ "gemini-3.7-flash", "claude-opus-5", "gpt-5.6",] as const;type ModelId = (typeof ALLOWED_MODELS)[number];type ChatMessage = { role: "system" | "user" | "assistant"; content: string;};function isModelId(value: unknown): value is ModelId { return ( typeof value === "string" && (ALLOWED_MODELS as readonly string[]).includes(value) );}function isChatMessage(value: unknown): value is ChatMessage { if (typeof value !== "object" || value === null) return false; const message = value as Record<string, unknown>; return ( ["system", "user", "assistant"].includes(String(message.role)) && typeof message.content === "string" && message.content.length > 0 && message.content.length <= 20_000 );}export async function POST(request: Request) { const apiKey = process.env.COMETAPI_API_KEY; const baseUrl = ( process.env.COMETAPI_BASE_URL || "https://api.cometapi.com/v1" ).replace(/\/$/, ""); if (!apiKey) { return Response.json( { error: "COMETAPI_API_KEY is not configured." }, { status: 500 }, ); } let payload: { model?: unknown; messages?: unknown }; try { payload = await request.json(); } catch { return Response.json({ error: "Invalid JSON body." }, { status: 400 }); } if (!isModelId(payload.model)) { return Response.json({ error: "Unsupported model ID." }, { status: 400 }); } if ( !Array.isArray(payload.messages) || payload.messages.length === 0 || payload.messages.length > 50 || !payload.messages.every(isChatMessage) ) { return Response.json( { error: "messages must contain 1 to 50 valid text messages." }, { status: 400 }, ); } const upstream = await fetch(`${baseUrl}/chat/completions`, { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: payload.model, messages: payload.messages, stream: true, }), cache: "no-store", signal: request.signal, }); if (!upstream.ok) { const requestId = upstream.headers.get("x-request-id"); console.error("CometAPI request failed", { status: upstream.status, requestId, }); return Response.json( { error: "The selected model request failed.", status: upstream.status, requestId, }, { status: upstream.status }, ); } if (!upstream.body) { return Response.json( { error: "The model returned no response body." }, { status: 502 }, ); } return new Response(upstream.body, { status: 200, headers: { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache, no-transform", }, });}
De handler geeft het disconnect-signaal van de browser upstream door, zodat het sluiten van het request onnodige generatie kan stoppen. Hij retourneert ook een geschoonde fout naar de client, terwijl diagnostische details in de serverlogs blijven.
Stap 5: Voeg modelselectie en stream-parsing toe
Maak een clientcomponent in app/page.tsx. Deze stuurt het geselecteerde model met het gesprek mee, parseert elk data:-event en voegt de incrementele delta.content-tekst toe aan het laatste assistant-bericht.
De browser roept CometAPI nooit rechtstreeks aan. Het enige doel is je eigen /api/chat-route, die de sleutel privé houdt en de allowlist afdwingt.
Kernprojectcode
De Route Handler hierboven is het complete app/api/chat/route.ts-bestand. Voeg de volgende pagina, layout en stylesheet toe om het uitvoerbare project af te ronden.
app/page.tsx
"use client";import { FormEvent, useState } from "react";const MODEL_OPTIONS = [ { id: "gemini-3.7-flash", label: "Gemini 3.7 Flash" }, { id: "claude-opus-5", label: "Claude Opus 5" }, { id: "gpt-5.6", label: "GPT-5.6" },] as const;type Role = "user" | "assistant";type Message = { role: Role; content: string };function textFromSseLine(line: string): string { const trimmed = line.trim(); if (!trimmed.startsWith("data:")) return ""; const data = trimmed.slice(5).trim(); if (!data || data === "[DONE]") return ""; try { const event = JSON.parse(data); return event.choices?.[0]?.delta?.content ?? ""; } catch { return ""; }}export default function Home() { const [model, setModel] = useState("gemini-3.7-flash"); const [messages, setMessages] = useState<Message[]>([]); const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); function appendAssistantText(text: string) { if (!text) return; setMessages((current) => { const next = [...current]; const lastIndex = next.length - 1; if (lastIndex >= 0 && next[lastIndex].role === "assistant") { next[lastIndex] = { ...next[lastIndex], content: next[lastIndex].content + text, }; } return next; }); } async function sendMessage(event: FormEvent<HTMLFormElement>) { event.preventDefault(); const content = input.trim(); if (!content || loading) return; const outgoing: Message[] = [...messages, { role: "user", content }]; setMessages([...outgoing, { role: "assistant", content: "" }]); setInput(""); setError(""); setLoading(true); try { const response = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model, messages: outgoing }), }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw new Error(body.error || `Request failed with ${response.status}`); } if (!response.body) throw new Error("Streaming is not available."); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { value, done } = await reader.read(); buffer += decoder.decode(value, { stream: !done }); const lines = buffer.split("\n"); buffer = lines.pop() ?? ""; for (const line of lines) { appendAssistantText(textFromSseLine(line)); } if (done) { appendAssistantText(textFromSseLine(buffer)); break; } } } catch (requestError) { setError( requestError instanceof Error ? requestError.message : "Request failed.", ); } finally { setLoading(false); } } return ( <main className="shell"> <section className="chat"> <header> <p className="eyebrow">Next.js + CometAPI</p> <h1>Multi-modelchat</h1> <label> Model <select value={model} onChange={(event) => setModel(event.target.value)} disabled={loading} > {MODEL_OPTIONS.map((option) => ( <option key={option.id} value={option.id}> {option.label} </option> ))} </select> </label> </header> <div className="messages" aria-live="polite"> {messages.length === 0 ? ( <p className="empty">Kies een model en stuur een bericht.</p> ) : ( messages.map((message, index) => ( <article className={message.role} key={`${message.role}-${index}`}> <b>{message.role === "user" ? "Jij" : "Assistent"}</b> <p>{message.content || "…"}</p> </article> )) )} </div> <form onSubmit={sendMessage}> <textarea value={input} onChange={(event) => setInput(event.target.value)} placeholder="Stel een vraag…" rows={3} maxLength={20_000} /> <button disabled={loading || !input.trim()} type="submit"> {loading ? "Streamen…" : "Verzenden"} </button> </form> {error ? <p className="error">{error}</p> : null} </section> </main> );}
app/layout.tsx
import type { Metadata } from "next";import "./globals.css";export const metadata: Metadata = { title: "Multi-Model Chat", description: "Een streamende Next.js-chatapp, aangedreven door CometAPI.",};export default function RootLayout({ children,}: Readonly<{ children: React.ReactNode }>) { return ( <html lang="en"> <body>{children}</body> </html> );}
app/globals.css
:root { color-scheme: dark; font-family: Arial, sans-serif; background: #07111f; color: #eef4ff;}* { box-sizing: border-box; }body { margin: 0; }button, select, textarea { font: inherit; }/* Layoutshell en chatkaart */.shell { min-height: 100vh; display: grid; place-items: center; padding: 32px 16px; }.chat { width: min(820px, 100%); background: #0d1b2e; border: 1px solid #223957; border-radius: 20px; padding: 24px; }/* Berichtenlijst en bubbels */.messages { min-height: 360px; display: grid; align-content: start; gap: 12px; margin: 24px 0; }.messages article { max-width: 85%; padding: 12px 14px; border-radius: 14px; white-space: pre-wrap; }.user { justify-self: end; background: #164f8f; }.assistant { justify-self: start; background: #182a42; }/* Formuliervelden, knoppen en foutstatussen volgen hetzelfde donkere thema. */
Stap 6: Start en deploy de app
npm run dev
Open http://localhost:3000, selecteer een model en stuur een bericht. Voor een productie-Node.js-deployment voeg je COMETAPI_API_KEY en COMETAPI_BASE_URL toe aan de server-side omgevingsinstellingen van de host en voer je vervolgens uit:
npm run buildnpm run start
Gebruik een hostingpad dat streamingresponses ondersteunt. Een statische export kan de /api/chat Route Handler niet draaien.
Test de streamingroute
Met de ontwikkelserver actief, roep je je backend direct aan:
curl -N http://localhost:3000/api/chat \ -H "Content-Type: application/json" \ -d '{ "model": "gemini-3.7-flash", "messages": [ {"role": "user", "content": "Leg modelroutering uit in twee zinnen."} ] }'
Een geslaagde aanvraag retourneert Server-Sent Events. De exacte ID’s en tekst variëren, maar de stream volgt dit patroon:
data: {"choices":[{"delta":{"content":"Model"}}]}data: {"choices":[{"delta":{"content":" routering"}}]}data: [DONE]
De browserparser leest elk SSE-event, extraheert choices[0].delta.content en voegt de gestreamde tekst toe aan het assistant-bericht zodra de chunks binnenkomen.
Veelvoorkomende integratiefouten
| Symptoom | Oorzaak | Oplossing |
|---|---|---|
| 401-authenticatiefout | Ontbrekende of ongeldige server-side sleutel | Stel COMETAPI_API_KEY in; stel deze niet bloot met NEXT_PUBLIC_. |
| 404 of verkeerde route | De basis-URL mist /v1 | Gebruik https://api.cometapi.com/v1. |
| 400 niet-ondersteund model | De ID staat niet in de backend-allowlist | Gebruik een exacte live tekstmodel-ID en werk beide selectors bij. |
| Het antwoord komt in één keer | De host of proxy buffert de eventstream | Schakel responstransformatie uit en gebruik een Node.js-deployment met streaming. |
Bereid de chatbackend voor op productie
- Authenticeer je eigen gebruikers. Stel geen publieke route bloot die credits uitgeeft voor anoniem verkeer.
- Rate-limit per gebruiker en IP. Beperk gelijktijdige streams, verzoeken per minuut, aantal berichten en berichtlengte.
- Houd de modellallowlist server-side. De browserselector is een gemak, geen beveiligingsgrens.
- Valideer de livecatalogus tijdens deployment. Raadpleeg
GEThttps://api.cometapi.com/api/modelsen laat de release falen als een geconfigureerde ID “upcoming” is, niet beschikbaar is of het chat-completions-eindpunt mist. - Volg kosten per route. Log het geselecteerde model, request-ID, latentie, tokenverbruik en gebruikers-ID. Stel indien nodig sleutelquota of uitgavenplafonds in op het CometAPI-dashboard.
- Handel disconnects en time-outs af. Bewaar
request.signal, stel een applicatie-time-out in en stop het werk wanneer de client weggaat. - Verberg configuratiefouten niet met een fallback. Geef 400- en 401-responses door. Gebruik een ander model alleen voor een begrensde set herstelbare fouten en alleen wanneer het requestschema compatibel is; zie de CometAPI Model Fallback Guide voor een tweelaagse fallbackketen (CometAPI primair → CometAPI fallbackmodel → officiële provider).
- Redigeer logs. Houd API-sleutels, volledige prompts en gevoelige modeloutput uit productiefoutlogs.
Eén Next.js-backend, meerdere modelkeuzes
De modelswitch hoort in je serverbeleid, niet in afzonderlijke provideraccounts. Een Next.js Route Handler kan één CometAPI-sleutel privé houden, per request een goedgekeurde model-ID accepteren en het geselecteerde model via één OpenAI-compatibel eindpunt streamen. De frontend blijft simpel, terwijl de backend de controle behoudt over toegang, validatie, observeerbaarheid en kosten. Met hetzelfde account en dezelfde sleutel kun je ook de native beeld- en video-API’s van CometAPI bereiken — zoals Flux voor beeldgeneratie en Kling voor videogeneratie — zodat deze chatbackend kan worden uitgebreid naar multimodale workflows zonder tweede integratie.
Gebruik de publieke modeldirectory van CometAPI voor modelontdekking en GET https://api.cometapi.com/api/models voor geautomatiseerde routeringsvalidatie.
Veelgestelde vragen
Kan ik meerdere AI-modellen gebruiken in één Next.js-app?
Ja. Houd één server-side route aan en geef bij elk request een allowlisted model-ID mee. De browser-UI kan modelkeuzes aanbieden, terwijl de server bepaalt welke ID’s worden geaccepteerd.
Hoe schakel ik tussen GPT, Claude en Gemini?
Stuur de geselecteerde model-ID in de requestbody mee. De server valideert deze tegen een allowlist en stuurt dezelfde chatpayload door naar het gekozen model via het OpenAI-compatibele eindpunt.
Waar moet ik mijn CometAPI API-sleutel opslaan?
Sla die op in een server-side omgevingsvariabele zoals COMETAPI_API_KEY. Stel deze nooit bloot via een NEXT_PUBLIC_-variabele of client-side code.
Maakt een OpenAI-compatibele API alle modellen onderling uitwisselbaar?
Nee. De requestvorm is overdraagbaar, maar modellen kunnen verschillen in ondersteunde parameters, contextlimieten, toolgedrag, outputstijl, latentie en prijs. Test elk allowlisted model met je productieprompts.
Kunnen Next.js Route Handlers AI-responses streamen?
Ja. Een Route Handler kan de upstream Server-Sent Events-stream retourneren met een text/event-stream contenttype, mits het deploymentplatform en eventuele proxy ervoor geen response bufferen.
Kan ik deze app deployen als een statische Next.js-site?
Nee. Een statische export kan de server-side /api/chat Route Handler niet draaien en kan de API-sleutel niet beschermen. Gebruik een Node.js- of andere compatibele serverruntime die streamingresponses ondersteunt.
