← Blog

Claude API Next.js Server Actions Example

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

If you're building a Next.js App Router project and want to call Claude from the server without spinning up a separate API route, Server Actions are the right tool. This article walks through a complete, working example: a form component that calls a Server Action, which calls Claude, and returns the response to the client — no client-side API keys, no extra /api route files required.

The short version: define an async function with "use server", call Claude's messages endpoint from inside it using fetch, and invoke that function directly from a form or button in your component. Below is the full pattern, including streaming and error handling, which is where most people get stuck.

Why Server Actions fit Claude API calls well

Server Actions run exclusively on the server, so your Claude API key never reaches the browser bundle. That's a meaningful win over calling Claude directly from client components, which would force you to either expose a key or proxy everything through a route handler anyway. Server Actions give you that proxy behavior for free, with less boilerplate than writing a route.ts file.

They're also a natural fit for form submissions — a chat input, a "summarize this" button, a feedback-to-response widget — where you want progressive enhancement and don't need a persistent streaming connection for every use case.

Basic example: a Server Action that calls Claude

Create app/actions.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.CLAUDE_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) {
    throw new Error(`Claude API error: ${response.status}`);
  }

  const data = await response.json();
  return data.content[0].text;
}

Then call it from a client component:

"use client";

import { useState } from "react";
import { askClaude } from "./actions";

export function AskForm() {
  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" required />
      <button type="submit" disabled={loading}>
        {loading ? "Thinking..." : "Ask Claude"}
      </button>
      <p>{answer}</p>
    </form>
  );
}

This works, but it has two practical issues: Server Actions don't stream natively back to the client in the same way a Route Handler with ReadableStream does, and you're responsible for retries, rate-limit handling, and usage tracking yourself.

Handling streaming with Server Actions

Server Actions can return a stream, but you need to wire it through useState updates manually since there's no built-in streaming primitive for Server Actions the way there is for Route Handlers. A common workaround is to use a Server Action to kick off the request, then read the stream via a Route Handler, or use libraries like the Vercel AI SDK which has first-class support for this pattern.

If you don't want to build your own streaming plumbing, routing through a hosted API that returns Server-Sent Events over a standard HTTP endpoint is simpler to integrate with fetch and ReadableStream directly in a Route Handler, which you then call from your client component instead of a Server Action. That tradeoff — Server Action for simple request/response, Route Handler for streaming — is common enough that most production Next.js apps use both.

Error handling and retries

Claude's API returns rate-limit and overload errors that you should handle explicitly rather than letting them bubble up as unhandled exceptions in your Server Action:

"use server";

export async function askClaude(prompt: string) {
  try {
    const response = await fetch("https://api.anthropic.com/v1/messages", {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "x-api-key": process.env.CLAUDE_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.status === 429) {
      return { error: "Rate limited, try again shortly." };
    }
    if (!response.ok) {
      return { error: `Request failed: ${response.status}` };
    }

    const data = await response.json();
    return { text: data.content[0].text };
  } catch (err) {
    return { error: "Network error calling Claude." };
  }
}

Returning an object with error or text instead of throwing keeps your client component logic simple and avoids Next.js's default error boundary behavior for expected failure cases like rate limits.

Using SubToAPI instead of raw Anthropic keys

If you're already paying for Claude through a personal or team subscription and don't want to manage a separate Anthropic API billing account, SubToAPI turns that access into a standard HTTPS API with sub_live_... keys. The Server Action code above barely changes — swap the endpoint and header:

"use server";

export async function askClaude(prompt: string) {
  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 }],
    }),
  });

  const data = await response.json();
  return data.content[0].text;
}

This gives you per-key usage metadata and team seat management on top of the same request shape, which is useful once more than one developer on your team is hitting Claude from the same Next.js app. See the quickstart and messages endpoint docs for the full request/response reference, and the streaming guide if you're building a chat-style Server Action that needs to feed a Route Handler.

A note on environment variables

Whether you use Anthropic directly or SubToAPI, keep the key in .env.local and never in a client component:

CLAUDE_API_KEY=sk-ant-...
# or
SUBTOAPI_KEY=sub_live_...

Server Actions already run server-side, so this is enforced by the framework — but it's worth double-checking you haven't accidentally imported the action file into a client component in a way that leaks the module's top-level code.

questions

Can a Next.js Server Action stream a Claude response directly to the UI? Not natively — Server Actions return a single resolved value by default. For token-by-token streaming, pair a Route Handler (which supports ReadableStream) with your client component, or use a library like the Vercel AI SDK that handles the streaming plumbing for you.

Do I need a separate /api route if I'm using Server Actions? No, for simple request/response calls to Claude, a Server Action replaces the need for a Route Handler entirely. You only need a Route Handler if you want true streaming or need the endpoint to be callable from outside your Next.js app.

Is it safe to put my Claude API key in a Server Action file? Yes, as long as you read it from process.env and the file is marked "use server". Next.js never bundles Server Action code into the client JavaScript, so the key stays server-side.

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 →