How to Integrate Claude AI with React: A Complete 2025 Guide
Step-by-step guide to adding Claude AI into a React application — from SDK setup to streaming responses and production error handling.

Building AI-powered features is no longer optional for competitive apps. Claude — Anthropic's frontier model — offers a best-in-class API that is safe, reliable, and genuinely useful for real product work. This guide walks you through a complete integration: from installing the SDK to streaming responses in a React UI.
Prerequisites
You need:
- A React project (Next.js App Router or Vite, both work)
- Node.js 18+ or Bun
- An Anthropic API key from console.anthropic.com
Setting Up the Anthropic SDK
Install the official SDK:
npm install @anthropic-ai/sdk
# or
bun add @anthropic-ai/sdkStore your API key as an environment variable — never expose it client-side:
# .env.local
ANTHROPIC_API_KEY=sk-ant-...Creating a Server-Side API Route
Never call the Anthropic API directly from the browser. Create a server-side route instead. In Next.js App Router:
// app/api/chat/route.ts
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
export async function POST(request: Request) {
const { messages } = await request.json();
const response = await client.messages.create({
model: 'claude-sonnet-4-5',
max_tokens: 1024,
messages,
});
const text = response.content[0].type === 'text'
? response.content[0].text
: '';
return Response.json({ text });
}Building the React Chat Component
Now wire up the frontend. Here's a minimal but complete chat component:
'use client';
import { useState } from 'react';
interface Message {
role: 'user' | 'assistant';
content: string;
}
export default function ChatComponent() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
async function sendMessage(e: React.FormEvent) {
e.preventDefault();
if (!input.trim() || loading) return;
const userMsg: Message = { role: 'user', content: input };
const newMessages = [...messages, userMsg];
setMessages(newMessages);
setInput('');
setLoading(true);
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: newMessages }),
});
const { text } = await res.json();
setMessages([...newMessages, { role: 'assistant', content: text }]);
} catch (err) {
console.error('Chat error:', err);
} finally {
setLoading(false);
}
}
return (
<div className="flex flex-col h-[600px] border rounded-xl overflow-hidden">
{/* Message list */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((msg, i) => (
<div key={i} className={msg.role === 'user' ? 'text-right' : 'text-left'}>
<span className="inline-block px-4 py-2 rounded-lg max-w-[80%] text-sm">
{msg.content}
</span>
</div>
))}
{loading && <p className="text-sm text-muted">Claude is thinking...</p>}
</div>
{/* Input */}
<form onSubmit={sendMessage} className="p-3 border-t flex gap-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask Claude anything..."
className="flex-1 px-3 py-2 rounded-lg border text-sm focus:outline-none"
/>
<button type="submit" disabled={loading}>
Send
</button>
</form>
</div>
);
}Adding Streaming Responses
Waiting for the full response feels slow. Streaming tokens as they arrive makes the UI feel instant. Update the API route to stream:
// app/api/chat/stream/route.ts
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
export async function POST(request: Request) {
const { messages } = await request.json();
const stream = await client.messages.stream({
model: 'claude-sonnet-4-5',
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' },
});
}And update the React component to read the stream:
async function sendMessageStreaming(e: React.FormEvent) {
e.preventDefault();
if (!input.trim() || loading) return;
const userMsg: Message = { role: 'user', content: input };
const newMessages = [...messages, userMsg];
setMessages([...newMessages, { role: 'assistant', content: '' }]);
setInput('');
setLoading(true);
const res = await fetch('/api/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: newMessages }),
});
const reader = res.body?.getReader();
const decoder = new TextDecoder();
if (!reader) return;
let accumulated = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
accumulated += decoder.decode(value, { stream: true });
setMessages((prev) => [
...prev.slice(0, -1),
{ role: 'assistant', content: accumulated },
]);
}
setLoading(false);
}Error Handling and Rate Limits
Production apps need proper error handling. Anthropic's SDK throws typed errors:
import Anthropic from '@anthropic-ai/sdk';
try {
const response = await client.messages.create({ ... });
} catch (error) {
if (error instanceof Anthropic.APIError) {
if (error.status === 429) {
// Rate limited — implement exponential backoff
console.error('Rate limited:', error.message);
} else if (error.status === 401) {
// Invalid API key
console.error('Auth error:', error.message);
} else {
console.error('API error:', error.status, error.message);
}
}
}What's Next
You now have a working Claude integration with streaming. From here:
- Add a system prompt in the
messages.createcall to set Claude's persona - Implement conversation memory by persisting messages to a database
- Add tool use to let Claude call your APIs
- Set up usage tracking so you can monitor costs per user
Claude's API is powerful and the documentation at docs.anthropic.com is excellent — genuinely one of the better API docs in the industry.