Claude API React Frontend Integration Guide
Can you call the Claude API directly from a React frontend?
No — not safely, and not in production. The Claude API (and any proxy like SubToAPI) requires an API key in the Authorization header, and anything shipped in a React bundle is visible to anyone who opens DevTools. If you put your key in fetch() calls from src/App.jsx, it will end up in the browser's network tab, in your built JS files, and eventually in someone's scraped list of leaked keys.
The correct pattern is: React frontend → your backend (or a proxy service) → Claude API. Your frontend never sees the real key. This guide walks through that architecture, shows working code for both a plain backend proxy and a managed proxy (SubToAPI), and covers streaming responses into a React UI — which is the part most tutorials skip or get wrong.
Why you can't skip the backend
Three concrete reasons:
- Key exposure — any
VITE_orREACT_APP_environment variable prefixed for client bundling gets inlined into static JS at build time and is fully readable. - CORS — Anthropic's API (and most LLM providers) is not designed to be called from arbitrary browser origins with a user-supplied key.
- Rate limiting and billing control — without a server layer, you can't cap usage per user, log requests, or swap providers later without touching the frontend.
This means every "React + Claude" integration is really "React + a small backend." The question is just how much backend you want to write yourself.
Option 1: Minimal Node/Express proxy
If you already have a backend, add one route that forwards to Claude:
// server.js
import express from "express";
import fetch from "node-fetch";
const app = express();
app.use(express.json());
app.post("/api/chat", async (req, res) => {
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-sonnet-4-5",
max_tokens: 1024,
messages: req.body.messages,
}),
});
const data = await response.json();
res.json(data);
});
app.listen(3001);
Your React app then calls /api/chat on your own origin, no CORS issues, no exposed key. This works, but you now own authentication, streaming plumbing, usage logging, and rate limiting yourself.
Option 2: SubToAPI as the backend layer
If you'd rather not build and maintain that proxy, SubToAPI gives you an HTTPS endpoint with its own sub_live_... application keys, so you can skip writing the Claude-forwarding server and just call SubToAPI directly from a backend route (still not from the browser — the same key-exposure rule applies).
// server.js — using SubToAPI instead of calling Anthropic directly
app.post("/api/chat", async (req, res) => {
const response = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SUBTOAPI_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 1024,
messages: req.body.messages,
}),
});
const data = await response.json();
res.json(data);
});
The practical difference is that SubToAPI keys come from your existing Claude access instead of a separate Anthropic billing account, and you get a dashboard with usage metadata and team seats out of the box — useful if more than one developer or product is hitting the same Claude access. Request shape follows the same /v1/messages convention documented at /docs/messages.
The React side: a simple chat component
Once the backend route exists, the frontend is provider-agnostic — it just talks to your own API:
import { useState } from "react";
export default function Chat() {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState("");
async function sendMessage() {
const newMessages = [...messages, { role: "user", content: input }];
setMessages(newMessages);
setInput("");
const res = await fetch("/api/chat", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ messages: newMessages }),
});
const data = await res.json();
setMessages([...newMessages, { role: "assistant", content: data.content[0].text }]);
}
return (
<div>
{messages.map((m, i) => (
<p key={i}><strong>{m.role}:</strong> {m.content}</p>
))}
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={sendMessage}>Send</button>
</div>
);
}
This works, but it feels sluggish for longer replies because the user sees nothing until the full response arrives.
Streaming into React
Claude supports server-sent events for token-by-token output, and this is where React integrations usually need the most care. On the backend, pass stream: true and pipe the response through; on the frontend, read the stream with ReadableStream and append chunks to state as they arrive:
async function sendMessageStreaming() {
const res = await fetch("/api/chat-stream", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ messages }),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let assistantText = "";
setMessages((prev) => [...prev, { role: "assistant", content: "" }]);
while (true) {
const { done, value } = await reader.read();
if (done) break;
assistantText += decoder.decode(value);
setMessages((prev) => {
const copy = [...prev];
copy[copy.length - 1] = { role: "assistant", content: assistantText };
return copy;
});
}
}
Your backend route needs to forward the raw stream rather than buffering it with res.json(). Details on event formats and how to parse them server-side are covered in /docs/streaming. This pattern is identical whether you're hitting Anthropic directly or a proxy — the SSE format is the same.
Handling tool use from a React UI
If your Claude integration calls tools (database lookups, search, calculators), the React side doesn't need to know about tool internals — your backend should resolve tool calls in a loop and only send the final text (or a structured result) to the frontend. See /docs/tools for the request/response shape if you're implementing this loop yourself. Trying to handle multi-step tool calls directly in frontend state usually turns into a mess of conditional rendering — keep that logic server-side.
Getting started quickly
If you want to skip writing a Claude-forwarding server from scratch, sign up at /signup, grab a sub_live_... key, and point your existing backend route at api.subtoapi.app instead of api.anthropic.com. The /docs/quickstart page has a minimal working example, and plans start at €9/month on the Solo tier — see /pricing for Team and Scale options if you need multiple seats.
Questions
Can I call Claude directly from React without any backend? Not safely. Any API key embedded in a React build is extractable by anyone viewing the page source or network requests, so a server-side proxy is required regardless of which provider or wrapper you use.
Does streaming work the same way with a proxy like SubToAPI as with the direct Claude API? Yes — SubToAPI exposes the same /v1/messages streaming format, so your frontend ReadableStream handling code doesn't change; only the backend's target URL and auth header differ.
What's the fastest way to add a working chat UI to an existing React app? Add one backend route that forwards to Claude or SubToAPI, build a simple fetch-based component like the one above, then upgrade to streaming once the basic request/response loop works end to end.