How to Build a Chatbot with Claude and React
If you're building a chatbot with Claude and React, the core challenge isn't the UI — it's wiring up a secure backend that talks to Claude's API, managing conversation state correctly, and rendering responses in a way that feels responsive. This guide walks through the full architecture: where Claude calls should happen, how to structure your React components, and what to watch out for when you move from a prototype to something users actually rely on.
The short version: never call Claude directly from the browser (your API key would be exposed), keep conversation history on the client or in a lightweight session store, and use a simple backend route — your own server or a hosted proxy like SubToAPI — to forward requests and return responses to React.
Architecture overview
A Claude chatbot in React has three layers:
- React frontend — input box, message list, loading/streaming states
- Backend proxy — holds your real API credentials, forwards requests, applies rate limits or auth
- Claude API — generates the response given the conversation history
The frontend never talks to Claude's API (or any LLM provider) directly. It talks to your own /api/chat route, which talks to Claude.
React UI → Your backend route → Claude API
← ←
This separation matters for two reasons: your API key stays server-side, and you get a single place to add logging, rate limiting, or cost controls later.
Setting up the backend route
A minimal Node.js/Express route that proxies chat requests looks like this:
app.post('/api/chat', async (req, res) => {
const { messages } = req.body;
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'x-api-key': process.env.ANTHROPIC_API_KEY,
'anthropic-version': '2023-06-01',
'content-type': 'application/json'
},
body: JSON.stringify({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 1024,
messages
})
});
const data = await response.json();
res.json(data);
});
If you'd rather skip managing raw Anthropic credentials, headers, and versioning yourself, SubToAPI sits in the same spot but issues scoped sub_live_... keys and a stable REST shape. The route above becomes a one-line change pointing at https://api.subtoapi.app/v1/messages with your SUBTOAPI_KEY. See the quickstart for the full request/response format.
Managing conversation state in React
Claude's API is stateless — every request needs the full message history. In React, that means your state shape should mirror what the API expects:
const [messages, setMessages] = useState([
{ role: 'user', content: 'Hi, who are you?' }
]);
When the user sends a new message, append it, call your backend with the full array, then append Claude's reply:
async function sendMessage(text) {
const updated = [...messages, { role: 'user', content: text }];
setMessages(updated);
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ messages: updated })
});
const data = await res.json();
setMessages([...updated, { role: 'assistant', content: data.content[0].text }]);
}
This is the pattern that most chatbot bugs trace back to: forgetting to send the full history, or mutating state instead of replacing it. Keep it immutable and explicit.
Building the UI
A chatbot UI needs three components at minimum:
- MessageList — renders
messages, differentiating user vs assistant bubbles - MessageInput — a controlled textarea with submit-on-enter
- ChatContainer — owns the
messagesstate and the send function
function MessageList({ messages }) {
return (
<div className="messages">
{messages.map((m, i) => (
<div key={i} className={`bubble ${m.role}`}>
{m.content}
</div>
))}
</div>
);
}
Keep the list auto-scrolling to the bottom on new messages with a useEffect that scrolls a ref into view — this is a small detail that makes a chatbot feel finished rather than half-built.
Handling loading and streaming
A non-streaming chatbot (request → wait → full response) is the simplest to build and good enough for most internal tools or MVPs. Show a typing indicator while waiting:
{isLoading && <div className="bubble assistant typing">●●●</div>}
For a production-facing chatbot, users expect tokens to appear as they're generated rather than waiting several seconds for a full paragraph. That requires a different data flow — Server-Sent Events or a ReadableStream from your backend to React — which is enough of its own topic that it deserves separate treatment; see streaming for the request parameters if you're using SubToAPI, or Claude's own streaming docs if you're calling Anthropic directly.
Adding tool use (optional)
If your chatbot needs to do more than chat — look up order status, query a database, call an internal API — you'll want Claude's tool use feature. You define a tool schema, Claude decides when to call it, and your backend executes the actual function before sending the result back. This is worth adding once your chatbot has a clear job beyond free-form conversation. SubToAPI's tools endpoint documents the schema if you want to avoid writing the tool-calling loop from scratch.
Error handling and rate limits
Three things will break a chatbot in production if you don't handle them:
- Network failures — wrap your fetch in try/catch and show a retry button, don't silently fail
- Rate limits — Claude (and any proxy in front of it) will return 429s under load; back off and retry rather than crashing the UI
- Long responses — set a sensible
max_tokensand truncate gracefully rather than letting a response run indefinitely
If you're running this for a team rather than a solo side project, usage visibility matters more than it seems at first — knowing which feature or which user is driving token spend saves you from surprise bills. That's one of the reasons teams reach for a managed layer like SubToAPI instead of raw API calls: the dashboard shows usage per key, and plans range from Solo at €9 up to Team and Scale seats for shared projects. See pricing for the breakdown, or start with a free trial at signup.
Deploying
Once the backend route and React frontend are working locally, deployment is straightforward: ship the backend as a serverless function or small Node service, and the React app as a static build. Keep the API key (Anthropic's or SubToAPI's) in environment variables, never in client bundle code — check your build output if you're unsure, since bundlers occasionally inline env vars that start with REACT_APP_ or VITE_ into the client bundle.
Questions
Do I need a backend if I'm just prototyping? Yes, even for a prototype. Browser-based requests to Claude's API expose your API key to anyone who opens dev tools. A minimal serverless function is enough — you don't need a full backend framework.
Can I use the same chatbot code with other LLM providers later? Mostly, if you keep your message format provider-agnostic in the React layer and isolate the API-specific logic in one backend function. Switching providers then means changing that one function, not your UI.
Should I stream responses from the start? Not necessarily. Build the request/response version first to get the state management and UI right, then add streaming once the core chatbot works — it's an enhancement to the data flow, not a prerequisite for a working app.