Claude API Next.js App Router Example
What you're actually trying to build
If you searched for "claude api next js app router example," you're probably trying to wire up a chat or completion feature in a Next.js 13+ app using the App Router, and the official docs don't give you a copy-pasteable route handler. This article walks through a working setup: a route.ts handler that calls Claude, a client component that streams tokens into the UI, and the edge-runtime details that trip people up.
The short version: you call Claude from a server-side route handler (never from the browser, since that exposes your API key), stream the response back using the Fetch API's streaming primitives, and consume it in a client component with useState and a ReadableStream reader. Below is the full pattern.
Project structure
A minimal App Router chat feature needs three pieces:
app/
api/
chat/
route.ts # server-side handler, calls Claude
chat/
page.tsx # server component wrapper
chat-client.tsx # client component with the UI
The route handler
App Router route handlers run on the server by default, which is exactly where your Claude API key belongs. Here's a non-streaming version first, since it's easier to reason about:
// app/api/chat/route.ts
export async function POST(req: Request) {
const { messages } = await req.json();
const res = 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-3-5-sonnet-20241022",
max_tokens: 1024,
messages,
}),
});
if (!res.ok) {
return new Response(await res.text(), { status: res.status });
}
const data = await res.json();
return Response.json(data);
}
This works and is a fine starting point. The problem: for anything longer than a short answer, your user stares at a blank screen until the whole response finishes generating.
Streaming with the App Router
Claude supports server-sent events when you pass "stream": true. In the App Router, you forward that stream directly as the Response body — no need for a custom SSE parser on your end, since Response accepts a ReadableStream.
// app/api/chat/route.ts
export const runtime = "edge";
export async function POST(req: Request) {
const { messages } = 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-3-5-sonnet-20241022",
max_tokens: 1024,
stream: true,
messages,
}),
});
return new Response(upstream.body, {
headers: { "content-type": "text/event-stream" },
});
}
Setting runtime = "edge" is optional but recommended for chat endpoints — it avoids cold-start latency and handles long-lived connections more gracefully than the Node.js runtime on most hosts.
Consuming the stream in a client component
On the client, read the response body as a stream and parse the SSE data: lines yourself. This is the part most tutorials skip:
// app/chat/chat-client.tsx
"use client";
import { useState } from "react";
export default function ChatClient() {
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
async function send() {
setOutput("");
const res = await fetch("/api/chat", {
method: "POST",
body: JSON.stringify({
messages: [{ role: "user", content: input }],
}),
});
const reader = res.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
for (const line of chunk.split("\n")) {
if (!line.startsWith("data:")) continue;
try {
const event = JSON.parse(line.slice(5));
if (event.type === "content_block_delta") {
setOutput((prev) => prev + event.delta.text);
}
} catch {
// ignore heartbeat / keep-alive lines
}
}
}
}
return (
<div>
<textarea value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={send}>Send</button>
<p>{output}</p>
</div>
);
}
This gives you the token-by-token typing effect users expect from a Claude-powered UI, without a WebSocket or a third-party SDK.
Simplifying the backend
The route handler above works, but you own the SSE parsing, error handling, key rotation, and usage tracking. If you'd rather skip that and just point your route handler at a hosted endpoint, SubToAPI turns your existing Claude access into an HTTPS API with a sub_live_... key, so the same route handler pattern above becomes a one-line change — swap the URL and header:
const upstream = 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-3-5-sonnet-20241022",
max_tokens: 1024,
stream: true,
messages,
}),
});
You get the same streaming behavior, plus per-key usage metadata and team seats if multiple developers are hitting the endpoint from different Next.js projects. See the quickstart and streaming docs for the full request/response shape, and pricing if you want to compare plans.
Common mistakes in App Router setups
- Calling Claude from a client component directly. This leaks your API key in the browser bundle. Always route through a server handler.
- Forgetting
runtime = "edge"or using it when you shouldn't. If your handler needs Node-only APIs (filesystem, certain crypto), keep it on the Node runtime instead. - Not handling partial SSE chunks. A single
reader.read()call can return multiple or partialdata:lines — buffer incomplete lines across chunks in production code rather than parsing each chunk in isolation like the simplified example above. - Skipping
content_block_startandmessage_stopevents. These matter for tool use and multi-block responses; if you're building anything beyond plain text, check the tools docs for the full event schema.
questions
Does the Next.js App Router support streaming Claude responses out of the box? Yes. Route handlers can return a ReadableStream directly as the Response body, and the Fetch API on both server and client supports reading that stream incrementally — no additional library is required.
Should I use the edge runtime or Node.js runtime for a Claude chat route? Use edge for pure streaming text endpoints — it starts faster and handles long connections well. Switch to Node.js if you need file access, heavier crypto, or Node-specific packages in the same handler.
Can I call Claude directly from a Server Component instead of a route handler? You can, for non-streaming use cases like generating page content at request time, but for interactive chat UIs you need a route handler so the client can open a fetch stream and update state as tokens arrive.