← Blog

Claude API React Frontend Integration Guide

2026-10-02 · 5 min read · SubToAPI Team

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:

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.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →