Claude API + Next.js Server Actions Example
Server Actions let you call the Claude API directly from a form submission or client component without writing a separate API route. You define an async function with "use server", call Claude inside it, and return the result to your UI — no fetch boilerplate, no manual CORS handling, no client-side API key.
This article walks through a complete, working example: a Next.js App Router project that sends a prompt to Claude from a Server Action, handles errors properly, and optionally streams the response back to the client. The same pattern works whether you're calling the Anthropic API directly or routing through a proxy like SubToAPI — the only difference is the base URL and key format.
Setting Up the Project
Assuming you already have a Next.js 14+ project with the App Router, create a dedicated file for your server actions:
app/actions/claude.ts
Store your API key in .env.local:
ANTHROPIC_API_KEY=sk-ant-...
Server Actions run on the server, so this environment variable is never exposed to the browser. That's the main security benefit over calling an LLM API from client-side JavaScript.
Basic Server Action Example
Here's a minimal Server Action that sends a user's message to Claude and returns the text response:
// app/actions/claude.ts
"use server";
export async function askClaude(prompt: string) {
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.ANTHROPIC_API_KEY!,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: prompt }],
}),
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`Claude API error: ${response.status} ${errorBody}`);
}
const data = await response.json();
return data.content[0].text;
}
And the client component that calls it:
// app/components/ChatForm.tsx
"use client";
import { useState } from "react";
import { askClaude } from "../actions/claude";
export default function ChatForm() {
const [answer, setAnswer] = useState("");
const [loading, setLoading] = useState(false);
async function handleSubmit(formData: FormData) {
setLoading(true);
const prompt = formData.get("prompt") as string;
const result = await askClaude(prompt);
setAnswer(result);
setLoading(false);
}
return (
<form action={handleSubmit}>
<textarea name="prompt" rows={4} className="w-full border p-2" />
<button type="submit" disabled={loading}>
{loading ? "Thinking..." : "Ask Claude"}
</button>
{answer && <p className="mt-4">{answer}</p>}
</form>
);
}
This works, but it has a known limitation: Server Actions return a single value when they finish, so the UI shows nothing until the full response is ready. For short answers that's fine. For longer completions, you want streaming.
Streaming from a Server Action
Server Actions don't natively support Server-Sent Events the way a Route Handler does, but you can still stream by returning a ReadableStream and consuming it with a hook like useFormState, or — more commonly in production — by calling a Route Handler instead for the streaming path and keeping Server Actions for simple, non-streamed requests.
If you want true token-by-token streaming in the browser, the more reliable pattern is:
// app/api/chat/route.ts
export async function POST(req: Request) {
const { prompt } = await req.json();
const upstream = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.ANTHROPIC_API_KEY!,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 1024,
stream: true,
messages: [{ role: "user", content: prompt }],
}),
});
return new Response(upstream.body, {
headers: { "Content-Type": "text/event-stream" },
});
}
Use this Route Handler for the streaming UI, and reserve Server Actions for form submissions where a single final result is acceptable — things like generating a summary, classifying text, or producing a draft that gets saved to a database.
Error Handling and Rate Limits
Claude API calls can fail for reasons outside your control: rate limits, overloaded models, invalid requests. Wrap your Server Action logic so failures surface as usable errors in the UI rather than crashing the request:
export async function askClaude(prompt: string) {
try {
const response = await fetch(/* ... */);
if (response.status === 429) {
throw new Error("Rate limited — try again in a moment.");
}
if (!response.ok) {
throw new Error(`Claude API error: ${response.status}`);
}
const data = await response.json();
return { success: true, text: data.content[0].text };
} catch (err) {
return { success: false, error: (err as Error).message };
}
}
Returning a structured object instead of throwing lets your client component render a friendly error state without an uncaught exception bubbling up through React.
Using SubToAPI Instead of a Raw Key
If you're building this for a team product rather than a personal project, calling the Anthropic API directly from your own key means every teammate and every environment shares one credential with no per-service visibility. SubToAPI turns your existing Claude access into an HTTPS API with scoped application keys (sub_live_...), so your Next.js app, staging environment, and internal tools each get their own key and usage breakdown.
The Server Action code barely changes — swap the URL and header:
const response = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.SUBTOAPI_KEY}`,
},
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: prompt }],
}),
});
See the quickstart and messages endpoint docs for the full request/response shape, and streaming docs if you want to proxy SSE through a Route Handler the way described above. Plans start at €9/month with a free trial — check pricing for team seat options.
Questions
Can Server Actions stream Claude's response token by token? Not natively with built-in React primitives in a simple way — Server Actions resolve once with a final value. For real token-by-token streaming, use a Route Handler returning the Claude response body as a stream and consume it on the client with fetch and a reader.
Is it safe to call the Claude API key directly from a Server Action? Yes — Server Actions run exclusively on the server, so environment variables like ANTHROPIC_API_KEY are never sent to the browser. This is one of the main reasons to use Server Actions over client-side fetch calls.
Do I need a separate backend to use Claude with Next.js? No. Server Actions and Route Handlers both run inside your Next.js app, so you can call Claude directly without deploying a separate API service, unless you need shared key management across multiple apps — in which case a proxy like SubToAPI handles that layer.