Claude API React Chatbot Example

Build a working Claude chatbot in React using the Anthropic SDK via a backend proxy. Full source for the React chat UI, streaming hooks, and a minimal Express or Next.js API route.

💥 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

Browser-side React cannot call the Claude API directly (your key would be exposed). The correct pattern: React UI → backend proxy (Express/Next.js) → Anthropic API. This guide shows the full stack.

Option A — Next.js App Router (recommended)

The Route Handler runs server-side, so no separate Express server is needed.

API route: app/api/chat/route.js

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

const client = new Anthropic(); // reads ANTHROPIC_API_KEY from env

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

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

  const encoder = new TextEncoder();
  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(encoder.encode(chunk.delta.text));
        }
      }
      controller.close();
    },
  });

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

React chat component: app/page.jsx

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

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

  async function send(e) {
    e.preventDefault();
    if (!input.trim() || streaming) return;

    const userMsg = { role: "user", content: input };
    const next = [...messages, userMsg];
    setMessages(next);
    setInput("");
    setStreaming(true);

    // Add empty assistant placeholder
    setMessages([...next, { role: "assistant", content: "" }]);

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

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

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      full += decoder.decode(value, { stream: true });
      setMessages([...next, { role: "assistant", content: full }]);
    }

    setStreaming(false);
  }

  return (
    <main style={{ maxWidth: 640, margin: "0 auto", padding: 24, fontFamily: "sans-serif" }}>
      <h1>Claude Chat</h1>
      <div style={{ minHeight: 300, border: "1px solid #ddd", borderRadius: 8, padding: 16, marginBottom: 16 }}>
        {messages.map((m, i) => (
          <div key={i} style={{ marginBottom: 12, textAlign: m.role === "user" ? "right" : "left" }}>
            <span style={{
              display: "inline-block", padding: "8px 12px", borderRadius: 16,
              background: m.role === "user" ? "#0070f3" : "#f0f0f0",
              color: m.role === "user" ? "#fff" : "#000",
            }}>{m.content}</span>
          </div>
        ))}
        {streaming && <p style={{ color: "#aaa" }}>Claude is typing…</p>}
      </div>
      <form onSubmit={send} style={{ display: "flex", gap: 8 }}>
        <input
          value={input}
          onChange={e => setInput(e.target.value)}
          placeholder="Ask Claude anything…"
          style={{ flex: 1, padding: 10, borderRadius: 8, border: "1px solid #ddd" }}
        />
        <button type="submit" disabled={streaming} style={{ padding: "10px 20px", borderRadius: 8 }}>
          Send
        </button>
      </form>
    </main>
  );
}

Option B — Create React App / Vite + Express proxy

Express proxy: server.js

import express from "express";
import Anthropic from "@anthropic-ai/sdk";
import cors from "cors";

const app = express();
app.use(cors({ origin: "http://localhost:5173" })); // Vite dev port
app.use(express.json());

const client = new Anthropic();

app.post("/api/chat", async (req, res) => {
  const { messages } = req.body;
  res.setHeader("Content-Type", "text/plain");
  res.setHeader("Transfer-Encoding", "chunked");

  const stream = await client.messages.stream({ model: "claude-sonnet-4-6", max_tokens: 1024, messages });
  for await (const chunk of stream) {
    if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
      res.write(chunk.delta.text);
    }
  }
  res.end();
});

app.listen(3001, () => console.log("Proxy on :3001"));

React hook: src/useClaudeStream.js

import { useState, useCallback } from "react";

export function useClaudeStream() {
  const [reply, setReply] = useState("");
  const [loading, setLoading] = useState(false);

  const send = useCallback(async (messages) => {
    setLoading(true);
    setReply("");
    const res = await fetch("http://localhost:3001/api/chat", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ messages }),
    });
    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    let full = "";
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      full += decoder.decode(value, { stream: true });
      setReply(full);
    }
    setLoading(false);
    return full;
  }, []);

  return { reply, loading, send };
}

Vercel AI SDK (simplest option)

// npm install ai @ai-sdk/anthropic
// app/api/chat/route.js
import { anthropic } from "@ai-sdk/anthropic";
import { streamText } from "ai";

export async function POST(req) {
  const { messages } = await req.json();
  const result = streamText({ model: anthropic("claude-sonnet-4-6"), messages });
  return result.toDataStreamResponse();
}

// components/Chat.jsx
import { useChat } from "ai/react";

export function Chat() {
  const { messages, input, handleInputChange, handleSubmit } = useChat();
  return (
    <form onSubmit={handleSubmit}>
      {messages.map(m => <div key={m.id}>{m.role}: {m.content}</div>)}
      <input value={input} onChange={handleInputChange} placeholder="Say something…" />
      <button type="submit">Send</button>
    </form>
  );
}

Approach comparison

ApproachSetup complexityStreamingBest for
Next.js Route Handler (manual)LowReadableStreamFull control, no extra deps
Vercel AI SDK + useChatVery lowBuilt-in hookRapid prototyping
CRA/Vite + Express proxyMediumfetch reader loopNon-Next.js React apps

To estimate the cost of your Claude chatbot, use the Claude API Cost Calculator. For streaming patterns in other frameworks, see the FastAPI streaming guide and Next.js detailed example.

Frequently asked questions

Can I call the Claude API directly from React?
No — calling the API from browser-side React exposes your API key in the client bundle. Always proxy requests through a backend (Express, Next.js Route Handler, or any Node.js server) that reads the key from an environment variable.
How do I stream Claude responses in React?
Use the Fetch API with `response.body.getReader()` to consume the SSE stream on the client. The hook in this guide handles buffering, partial chunks, and cleanup.
Which React framework works best with Claude?
Next.js App Router is the most convenient — Route Handlers run server-side so no CORS proxy is needed. Create React App or Vite work too, but need a separate Express proxy server.
Can I use the Vercel AI SDK instead?
Yes. `@ai-sdk/anthropic` with `useChat()` from `ai/react` reduces the streaming hook to 5 lines. The manual approach in this guide is useful when you need more control over the request.
How do I maintain conversation history in React?
Store the message array in `useState`. Append each user message before the API call and each assistant reply after. Pass the full array to the API as the `messages` parameter.

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