Skip to main content
    Guide

    Add AI to a Next.js App

    Call Behest directly from your Next.js client components — CORS is handled, no API route proxy needed. Or use server components for SSR. This guide covers both approaches with working code examples.

    Why Behest for Next.js

    Normally, adding AI to a Next.js app means creating API routes to proxy requests to OpenAI or Anthropic. You do this because LLM providers do not support CORS — you cannot call them directly from the browser without exposing your API keys.

    Behest eliminates this problem. It handles CORS natively, so your Next.js client components can call Behest directly using fetch() — no API route proxy, no serverless function, no backend code. Behest also handles authentication, rate limiting, PII scrubbing, prompt injection defense, and conversation memory, so you do not need to build any of that yourself.

    This means fewer files, less infrastructure, and faster time to production. Whether you are building a chatbot, an AI-powered search, or any feature that needs an LLM, Behest gives you a complete AI backend in one API call.

    Prerequisites

    • A Next.js 13+ app (App Router or Pages Router)
    • A Behest AI account with a project and API key (sign up free)
    • Your app's origin added to your project's CORS settings (e.g., http://localhost:3000)

    Client Component — Call from the Browser

    This is the recommended approach for interactive features like chat, search, and real-time AI responses. The component calls Behest directly from the browser — no API route needed. CORS is handled by Behest, and your API key authenticates the request securely with project-level and user-level isolation.

    "use client";
    
    import { useState } from "react";
    
    const BEHEST_URL = "https://your-project.behest.app/v1/chat/completions";
    const API_KEY = "your-api-key";
    
    export function AiChat() {
      const [input, setInput] = useState("");
      const [messages, setMessages] = useState<
        { role: string; content: string }[]
      >([]);
      const [loading, setLoading] = useState(false);
      const [error, setError] = useState<string | null>(null);
    
      async function handleSubmit(e: React.FormEvent) {
        e.preventDefault();
        if (!input.trim() || loading) return;
    
        const userMessage = { role: "user", content: input };
        setMessages((prev) => [...prev, userMessage]);
        setInput("");
        setLoading(true);
        setError(null);
    
        try {
          const response = await fetch(BEHEST_URL, {
            method: "POST",
            headers: {
              "Authorization": `Bearer ${API_KEY}`,
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              model: "gemini-2.5-flash",
              messages: [...messages, userMessage],
            }),
          });
    
          if (!response.ok) {
            if (response.status === 429) {
              throw new Error("Rate limited. Please wait a moment.");
            }
            throw new Error(`Request failed: ${response.status}`);
          }
    
          const data = await response.json();
          const assistantMessage = data.choices[0].message;
          setMessages((prev) => [...prev, assistantMessage]);
        } catch (err) {
          setError(err instanceof Error ? err.message : "Something went wrong");
        } finally {
          setLoading(false);
        }
      }
    
      return (
        <div className="max-w-xl mx-auto p-4">
          <div className="space-y-4 mb-4">
            {messages.map((msg, i) => (
              <div key={i} className="p-3 rounded-lg bg-muted">
                <strong>{msg.role === "user" ? "You" : "AI"}:</strong>
                <p>{msg.content}</p>
              </div>
            ))}
            {loading && <p className="text-muted-foreground">Thinking...</p>}
            {error && <p className="text-red-500">{error}</p>}
          </div>
          <form onSubmit={handleSubmit} className="flex gap-2">
            <input
              type="text"
              value={input}
              onChange={(e) => setInput(e.target.value)}
              placeholder="Ask something..."
              disabled={loading}
              className="flex-1 px-3 py-2 border rounded-md"
            />
            <button
              type="submit"
              disabled={loading}
              className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
            >
              Send
            </button>
          </form>
        </div>
      );
    }

    Notice there is no app/api/chat/route.ts needed. Behest handles CORS, so the browser can call it directly. With OpenAI, you would need an API route to proxy requests and hide your key. With Behest, the key authenticates at the project level, and tenant isolation protects each user.

    Server Component — Call During SSR

    For content that should be rendered server-side — like an AI-generated product description or a pre-loaded summary — you can call Behest from a Next.js server component. The response is included in the HTML sent to the browser, improving performance and SEO.

    // app/summary/page.tsx (Server Component — no "use client")
    
    const BEHEST_URL = "https://your-project.behest.app/v1/chat/completions";
    const API_KEY = process.env.BEHEST_API_KEY!;
    
    async function getAISummary(topic: string): Promise<string> {
      const response = await fetch(BEHEST_URL, {
        method: "POST",
        headers: {
          "Authorization": `Bearer ${API_KEY}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          model: "gemini-2.5-flash",
          messages: [
            {
              role: "user",
              content: `Summarize ${topic} in 2-3 sentences.`,
            },
          ],
        }),
        next: { revalidate: 3600 }, // Cache for 1 hour
      });
    
      if (!response.ok) {
        throw new Error("Failed to generate summary");
      }
    
      const data = await response.json();
      return data.choices[0].message.content;
    }
    
    export default async function SummaryPage() {
      const summary = await getAISummary("Next.js 16 features");
    
      return (
        <main>
          <h1>AI-Generated Summary</h1>
          <p>{summary}</p>
        </main>
      );
    }

    Server-side calls do not require CORS since they run on the server. You can store your API key in an environment variable and use Next.js caching with next.revalidate to control how often the AI response is refreshed.

    Comparison: OpenAI Direct vs Behest

    When you call OpenAI directly from a Next.js app, you need to create an API route to proxy the request and hide your key. With Behest, you can call directly from a client component:

    OpenAI: 2 files needed

    • app/api/chat/route.ts — API route proxy
    • Client component calling /api/chat
    • Must hide API key server-side
    • No auth, rate limiting, or PII protection

    Behest: 1 file needed

    • Client component calling Behest directly
    • CORS handled — no proxy needed
    • Auth, rate limiting, PII built in
    • Memory persists across sessions

    Per-User Tracking

    Pass the X-End-User-Id header to enable per-user memory, rate limiting, and analytics. This is especially useful in Next.js apps where you already have user authentication via NextAuth, Clerk, or similar:

    const response = await fetch(BEHEST_URL, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${API_KEY}`,
        "Content-Type": "application/json",
        "X-End-User-Id": session.user.id, // From NextAuth, Clerk, etc.
      },
      body: JSON.stringify({
        model: "gemini-2.5-flash",
        messages: [{ role: "user", content: input }],
      }),
    });

    With the user ID header, Behest maintains separate conversation memory per user, enforces per-user rate limits, and tracks per-user token usage in the analytics dashboard. Each user gets isolated context — one user's conversation never leaks into another's.

    What Behest Handles for You

    Every request through Behest automatically gets these features without any additional code:

    • CORS — call from the browser, no proxy needed
    • Authentication — project-level API keys with tenant isolation
    • Rate Limiting — three-tier: per-IP, per-project, per-user
    • PII Scrubbing — Microsoft Presidio detects and redacts PII before it reaches the LLM
    • Prompt Defense — Sentinel blocks prompt injection attacks
    • Memory — persistent conversation context across sessions
    • Observability — OpenTelemetry + Grafana dashboards for every request