Building a Streaming AI Chat App with Next.js 16 + Claude API — Complete App Router Guide Tutorial for building a streaming AI chat application using Next.js 16 and the Anthropic Claude API, emphasizing a from-scratch approach with only the Anthropic SDK rather than the Vercel AI SDK. The guide explains how to implement a server-side Route Handler that streams Claude's responses to a client component using Web Streams API, ensuring the API key remains secure by keeping it exclusively on the server. The post notes that as of May 2026, `create-next-app` installs Next.js 16.2.6 with React 19.2.4, and the entire streaming backend can be built in approximately 50 lines of code. Search for "Next.js AI chat" in 2026 and Vercel AI SDK still comes up as the de facto standard. Nothing wrong with it, but relying on the SDK means you often don't understand what's happening under the hood — how streaming actually works, what the Route Handler is doing behind the scenes. I built this from scratch in a sandbox using only the Anthropic SDK. The entire flow: create-next-app , adding @anthropic-ai/sdk , implementing the Route Handler. It turned out simpler than I expected. About 50 lines gets you a production-deployable streaming chat backend. One thing I noticed while doing this: create-next-app@latest now installs Next.js 16 . Most tutorials you'll find are still targeting Next.js 14 or 15. This post reflects what you actually get in May 2026. What We're Building The app structure: - Next.js 16.2.6 + App Router - Route Handler /api/chat calling Claude API server-side - SSE Server-Sent Events delivering streaming responses to the client - React 19 "use client" component rendering the stream in real time The key point: the API key is read only on the server and never included in the client bundle. This is a direct consequence of how Next.js App Router separates server and client code. Actual build output from the sandbox: ▲ Next.js 16.2.6 Turbopack ✓ Compiled successfully in 1874ms Route app ┌ ○ / Static prerendered as static content └ ƒ /api/chat Dynamic server-rendered on demand Project Structure When finished, the structure looks like this. Two files are the core; the rest is generated by create-next-app . nextjs-claude-chat/ ├── src/ │ └── app/ │ ├── api/ │ │ └── chat/ │ │ └── route.ts ← Claude API streaming endpoint core │ ├── page.tsx ← Chat UI core │ ├── layout.tsx ← Auto-generated │ └── globals.css ← Auto-generated ├── .env.local ← ANTHROPIC API KEY goes here ├── package.json └── tsconfig.json Two files. route.ts is server code; page.tsx is client code. api/chat/route.ts ends up in the server bundle only, while page.tsx with its "use client" directive goes to the client bundle. This separation is what makes API key security work. Prerequisites - Node.js 18+ - Anthropic API key sk-ant-... — get one at console.anthropic.com https://console.anthropic.com - Basic TypeScript knowledge - Basic Next.js App Router understanding you can follow along without it Step 1: Create the Project and Install Dependencies npx create-next-app@latest nextjs-claude-chat \ --typescript \ --tailwind \ --eslint \ --app \ --src-dir \ --import-alias "@/ " cd nextjs-claude-chat npm install @anthropic-ai/sdk As of May 2026, create-next-app@latest installs Next.js 16.2.6 with React 19.2.4. Existing tutorials using Next.js 14/15 may have minor differences. Key dependencies after installation: { "dependencies": { "@anthropic-ai/sdk": "^0.97.1", "next": "16.2.6", "react": "19.2.4", "react-dom": "19.2.4" } } Anthropic SDK 0.97.x is the current latest. Earlier versions 0.20.x and below had a different messages.stream API, so pin your version if you're migrating. Step 2: Implement the Claude API Route Handler The core file. Create src/app/api/chat/route.ts : python import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic { apiKey: process.env.ANTHROPIC API KEY, } ; export async function POST req: Request { const { messages } = await req.json ; const stream = await client.messages.stream { model: "claude-opus-4-7", max tokens: 1024, messages, } ; const encoder = new TextEncoder ; const readable = new ReadableStream { async start controller { for await const chunk of stream { if chunk.type === "content block delta" && chunk.delta.type === "text delta" { controller.enqueue encoder.encode data: ${JSON.stringify { text: chunk.delta.text } }\n\n ; } } controller.enqueue encoder.encode "data: DONE \n\n" ; controller.close ; }, } ; return new Response readable, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", }, } ; } Two things worth noting here. First, client.messages.stream returns an AsyncIterableStream. The for await...of loop receives chunks one at a time and pushes them to the client. When the stream ends, a DONE signal is sent and the controller closes. Second, ReadableStream + TextEncoder is Web Streams API standard. Next.js Route Handlers use Web Streams, not Node.js stream module. This is why the code looks different from FastAPI streaming https://dev.to/en/blog/en/fastapi-claude-api-streaming-production-guide-2026 or Express implementations. new ReadableStream may feel unfamiliar, but it's the standard across modern JavaScript runtimes — Cloudflare Workers, Deno, Bun all work the same way.The filter on content block delta events: Anthropic's streaming protocol emits multiple event types message start , content block start , content block delta , message delta , message stop . Only text delta typed content block delta events carry actual text content. Step 3: Environment Variables and Security Create .env.local in the project root same level as .next/ : ANTHROPIC API KEY=sk-ant-your-actual-key-here Never use the NEXT PUBLIC prefix. This is core to Next.js security. | Variable format | Accessible from | Use for | |---|---|---| ANTHROPIC API KEY | Server only Route Handler, Server Component | ✓ API keys | NEXT PUBLIC API KEY | Client-public included in browser bundle | ✗ Never use for API keys | NEXT PUBLIC variables get inlined into the JavaScript bundle at build time. Anyone can see them in browser DevTools. Without the prefix, the variable is server-only — referencing it from client code will cause a build error. Step 4: Client Chat UI Create src/app/page.tsx with streaming state management: js "use client"; import { useState, useRef, useEffect } from "react"; type Message = { role: "user" | "assistant"; content: string; }; export default function ChatPage { const messages, setMessages = useState