Claude API in Next.js

Build a streaming Claude AI chatbot in Next.js using the App Router. Route handler, useChat hook, and full TypeScript example with Anthropic SDK.

💥 50p impulse-buy: Power Prompts PDF (first 10 buyers) 30 battle-tested Claude Code prompts · 8-page PDF · paste into CLAUDE.md and never re-type a prompt again · 50p impulse-buy, no commitment

Project setup

npx create-next-app@latest my-claude-app --typescript --app
cd my-claude-app
npm install @anthropic-ai/sdk

Add your key to .env.local (never prefix with NEXT_PUBLIC_):

ANTHROPIC_API_KEY=sk-ant-...

Route Handler — app/api/chat/route.ts

import Anthropic from "@anthropic-ai/sdk";
import { NextRequest } from "next/server";

const client = new Anthropic();

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

  const stream = await client.messages.create({
    model: "claude-sonnet-4-6-20251001",
    max_tokens: 1024,
    stream: true,
    messages,
  });

  const readable = new ReadableStream({
    async start(controller) {
      for await (const event of stream) {
        if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
          controller.enqueue(new TextEncoder().encode(event.delta.text));
        }
      }
      controller.close();
    },
  });

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

Client component — app/page.tsx

"use client";
import { useState, useRef, useEffect } from "react";

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

export default function ChatPage() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState("");
  const [streaming, setStreaming] = useState(false);

  async function send() {
    if (!input.trim() || streaming) return;
    const userMsg: Message = { role: "user", content: input };
    const newMessages = [...messages, userMsg];
    setMessages([...newMessages, { role: "assistant", content: "" }]);
    setInput("");
    setStreaming(true);

    const res = await fetch("/api/chat", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ messages: newMessages }),
    });

    const reader = res.body!.getReader();
    const decoder = new TextDecoder();
    let done = false;
    while (!done) {
      const { value, done: dr } = await reader.read();
      done = dr;
      const chunk = decoder.decode(value, { stream: !done });
      setMessages((m) => {
        const last = m[m.length - 1];
        return [...m.slice(0, -1), { ...last, content: last.content + chunk }];
      });
    }
    setStreaming(false);
  }

  return (
    <main style={{ maxWidth: 640, margin: "2rem auto", padding: "0 1rem" }}>
      <h1>Claude Chat</h1>
      {messages.map((m, i) => (
        <div key={i}><strong>{m.role === "user" ? "You" : "Claude"}:</strong>
          <p style={{ whiteSpace: "pre-wrap" }}>{m.content}</p>
        </div>
      ))}
      <input value={input} onChange={(e) => setInput(e.target.value)}
        onKeyDown={(e) => e.key === "Enter" && send()} placeholder="Ask Claude…" />
      <button onClick={send} disabled={streaming}>{streaming ? "…" : "Send"}</button>
    </main>
  );
}

Using the Vercel AI SDK (less boilerplate)

npm install ai @ai-sdk/anthropic
// app/api/chat/route.ts
import { anthropic } from "@ai-sdk/anthropic";
import { streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
  const { messages } = await req.json();
  return streamText({ model: anthropic("claude-sonnet-4-6-20251001"), messages }).toDataStreamResponse();
}
// app/page.tsx
"use client";
import { useChat } from "ai/react";
export default function Chat() {
  const { messages, input, handleInputChange, handleSubmit } = useChat();
  return (
    <form onSubmit={handleSubmit}>
      {messages.map((m) => <div key={m.id}><b>{m.role}</b>: {m.content}</div>)}
      <input value={input} onChange={handleInputChange} placeholder="Message…" />
      <button>Send</button>
    </form>
  );
}

Approach comparison

ApproachProsCons
Raw Anthropic SDKNo extra deps, full control over streamingManual stream piping boilerplate
Vercel AI SDK + @ai-sdk/anthropicuseChat hook, auto retry, 10 lines totalExtra dependency, Vercel-specific patterns

Estimate request costs with the Claude API Cost Calculator.

Frequently asked questions

Should I call the Claude API from a Next.js server action or a route handler?
Use a Route Handler (app/api/chat/route.ts) for streaming responses. Server Actions do not yet support streaming ReadableStream responses. Route Handlers let you return a StreamingTextResponse or a raw ReadableStream, which is required for token-by-token chat UI.
Where do I store my ANTHROPIC_API_KEY in a Next.js project?
Add ANTHROPIC_API_KEY to your .env.local file. Never prefix it with NEXT_PUBLIC_ — that would expose it to the browser bundle. Next.js automatically makes non-NEXT_PUBLIC_ variables available in server-side code.
How do I stream Claude responses to the browser in Next.js?
In your Route Handler, call client.messages.create with stream: true to get an async iterable. Pipe text deltas into a TransformStream and return new Response(readable). The Vercel AI SDK handles all of this automatically via toDataStreamResponse().
Can I use the Vercel AI SDK with Claude?
Yes. The Vercel AI SDK has an official Anthropic provider (@ai-sdk/anthropic). Use streamText with the anthropic() provider to get a drop-in streaming solution with the useChat hook.
How do I add conversation memory to a Next.js Claude chatbot?
The Claude API is stateless — send the full messages array in every request. On the client, append each user message and assistant reply to a useState messages array before sending the next request.

Free tools

Cost Calculator → API Cookbook → Diff Summarizer → Skills Browser →

More examples

Claude API Python QuickstartClaude API Node.js / TypeScript QuickstartClaude API Streaming in PythonClaude API Streaming in Node.js / TypeScriptClaude API Tool Use in PythonClaude API Tool Use in Node.js / TypeScript