← Blog

Claude API + Next.js Server Actions Example

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

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.

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 →