We're in development! Things may crash or break

Examples

Next.js Route Handlers

Track LLM calls made inside Next.js Route Handlers (app/api) — useful for streaming responses and third-party webhooks.

When to use route handlers vs. server actions

Use route handlers (app/api/) when:

  • You need to stream a response to the browser (Server Actions can't stream yet)
  • You're building an endpoint consumed by external clients (mobile apps, CLI tools)
  • You need webhook handling that calls an LLM

1. Non-streaming chat endpoint

// app/api/chat/route.ts
import { auth } from "@/lib/auth";
import { openai } from "@/lib/openai";
import { NextRequest, NextResponse } from "next/server";

type Message = { role: "user" | "assistant"; content: string };

export async function POST(req: NextRequest) {
  const session = await auth();
  if (!session?.user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const { messages, conversationId } = (await req.json()) as {
    messages: Message[];
    conversationId: string;
  };

  const completion = await openai
    .withContext({
      feature: "chat-api",
      user: session.user.id,
      conversationId,
    })
    .chat.completions.create({
      model: "gpt-4o",
      messages: [
        { role: "system", content: "You are a helpful assistant." },
        ...messages,
      ],
    });

  const content = completion.choices[0].message.content;

  return NextResponse.json({ content });
}

2. Streaming chat endpoint

Use the Vercel AI SDK ReadableStream pattern. FluxGate tracks the full event after the stream closes.

// app/api/chat/stream/route.ts
import { auth } from "@/lib/auth";
import { openai } from "@/lib/openai";
import { NextRequest } from "next/server";

export async function POST(req: NextRequest) {
  const session = await auth();
  if (!session?.user) {
    return new Response("Unauthorized", { status: 401 });
  }

  const { messages, conversationId } = await req.json();

  const stream = await openai
    .withContext({
      feature: "streaming-chat",
      user: session.user.id,
      conversationId,
    })
    .chat.completions.create({
      model: "gpt-4o",
      messages: [
        { role: "system", content: "You are a helpful assistant." },
        ...messages,
      ],
      stream: true,
    });

  // Convert OpenAI AsyncIterable to a Web ReadableStream
  const readable = new ReadableStream({
    async start(controller) {
      for await (const chunk of stream) {
        const delta = chunk.choices[0]?.delta?.content ?? "";
        if (delta) {
          controller.enqueue(new TextEncoder().encode(delta));
        }
      }
      controller.close();
      // FluxGate tracking is finalised here, after the stream closes
    },
  });

  return new Response(readable, {
    headers: {
      "Content-Type": "text/plain; charset=utf-8",
      "Transfer-Encoding": "chunked",
    },
  });
}

Client-side consumption:

// app/dashboard/chat/_components/chat.tsx
"use client";

async function sendMessage(messages: Message[], conversationId: string) {
  const res = await fetch("/api/chat/stream", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ messages, conversationId }),
  });

  const reader = res.body!.getReader();
  const decoder = new TextDecoder();
  let fullText = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    const chunk = decoder.decode(value, { stream: true });
    fullText += chunk;
    // update your React state here to render the chunk progressively
  }

  return fullText;
}

3. Webhook → LLM pipeline

Process incoming events (e.g. a new support ticket) and generate an LLM response automatically.

// app/api/webhooks/support/route.ts
import { anthropic } from "@/lib/anthropic";
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";

function verifySignature(body: string, signature: string): boolean {
  const expected = crypto
    .createHmac("sha256", process.env.WEBHOOK_SECRET!)
    .update(body)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(`sha256=${expected}`),
  );
}

export async function POST(req: NextRequest) {
  const rawBody = await req.text();
  const sig = req.headers.get("x-webhook-signature") ?? "";

  if (!verifySignature(rawBody, sig)) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
  }

  const ticket = JSON.parse(rawBody) as {
    id: string;
    userId: string;
    subject: string;
    body: string;
  };

  const message = await anthropic
    .withContext({
      feature: "support-triage",
      user: ticket.userId,
      sessionId: ticket.id,
    })
    .messages.create({
      model: "claude-sonnet-4-6",
      max_tokens: 512,
      messages: [
        {
          role: "user",
          content: `Triage this support ticket and suggest a priority (low/medium/high) and a one-sentence reply draft.\n\nSubject: ${ticket.subject}\n\n${ticket.body}`,
        },
      ],
    });

  const triage =
    message.content[0].type === "text" ? message.content[0].text : "";

  return NextResponse.json({ triage });
}

4. Embedding generation endpoint

// app/api/embeddings/route.ts
import { auth } from "@/lib/auth";
import { openai } from "@/lib/openai";
import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
  const session = await auth();
  if (!session?.user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const { input } = await req.json();

  const result = await openai
    .withContext({ feature: "semantic-search", user: session.user.id })
    .embeddings.create({
      model: "text-embedding-3-small",
      input,
    });

  return NextResponse.json({ embedding: result.data[0].embedding });
}