惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
The Exploit Database - CXSecurity.com
F
Fortinet All Blogs
U
Unit 42
F
Full Disclosure
雷峰网
雷峰网
博客园 - 司徒正美
云风的 BLOG
云风的 BLOG
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tailwind CSS Blog
The Cloudflare Blog
Last Week in AI
Last Week in AI
罗磊的独立博客
D
DataBreaches.Net
C
Check Point Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
O
OpenAI News
C
CXSECURITY Database RSS Feed - CXSecurity.com
aimingoo的专栏
aimingoo的专栏
S
Security @ Cisco Blogs
大猫的无限游戏
大猫的无限游戏
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
S
SegmentFault 最新的问题
NISL@THU
NISL@THU
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Hacker News
The Hacker News
Webroot Blog
Webroot Blog
Security Latest
Security Latest
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Google DeepMind News
Google DeepMind News
酷 壳 – CoolShell
酷 壳 – CoolShell
N
News | PayPal Newsroom
P
Proofpoint News Feed
B
Blog RSS Feed
MongoDB | Blog
MongoDB | Blog
C
Cybersecurity and Infrastructure Security Agency CISA
N
News and Events Feed by Topic
Google Online Security Blog
Google Online Security Blog
H
Help Net Security
Spread Privacy
Spread Privacy
T
Threat Research - Cisco Blogs
GbyAI
GbyAI
I
Intezer
Application and Cybersecurity Blog
Application and Cybersecurity Blog
M
MIT News - Artificial intelligence
Vercel News
Vercel News
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
IT之家
IT之家
MyScale Blog
MyScale Blog
腾讯CDC

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Building a Streaming AI Chat App with Next.js 16 + Claude API — Complete App Router Guide
Jangwook Kim · 2026-05-20 · via DEV Community

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

Enter fullscreen mode Exit fullscreen mode

Next.js 16 + Claude API Architecture Diagram

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

Enter fullscreen mode Exit fullscreen mode

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
  • 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

Enter fullscreen mode Exit fullscreen mode

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"
  }
}

Enter fullscreen mode Exit fullscreen mode

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:

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",
    },
  });
}

Enter fullscreen mode Exit fullscreen mode

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 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

Enter fullscreen mode Exit fullscreen mode

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:

"use client";

import { useState, useRef, useEffect } from "react";

type Message = {
  role: "user" | "assistant";
  content: string;
};

export default function ChatPage() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState("");
  const [isLoading, setIsLoading] = useState(false);
  const bottomRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    bottomRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [messages]);

  const sendMessage = async () => {
    if (!input.trim() || isLoading) return;

    const userMessage: Message = { role: "user", content: input };
    const updatedMessages = [...messages, userMessage];
    setMessages(updatedMessages);
    setInput("");
    setIsLoading(true);

    // Add placeholder for the assistant response
    const assistantMessage: Message = { role: "assistant", content: "" };
    setMessages([...updatedMessages, assistantMessage]);

    const res = await fetch("/api/chat", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ messages: updatedMessages }),
    });

    if (!res.body) return;

    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);
      const lines = chunk.split("\n");

      for (const line of lines) {
        if (line.startsWith("data: ") && line !== "data: [DONE]") {
          const data = JSON.parse(line.slice(6));
          setMessages((prev) => {
            const last = prev[prev.length - 1];
            return [
              ...prev.slice(0, -1),
              { ...last, content: last.content + data.text },
            ];
          });
        }
      }
    }

    setIsLoading(false);
  };

  return (
    <main className="flex flex-col h-screen max-w-2xl mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">Claude Chat</h1>
      <div className="flex-1 overflow-y-auto space-y-4 mb-4">
        {messages.map((msg, i) => (
          <div key={i} className={`p-3 rounded-lg ${
            msg.role === "user"
              ? "bg-blue-100 ml-auto max-w-xs"
              : "bg-gray-100 mr-auto max-w-md"
          }`}>
            <span className="text-xs text-gray-500 block mb-1">
              {msg.role === "user" ? "You" : "Claude"}
            </span>
            <p className="whitespace-pre-wrap">{msg.content}</p>
          </div>
        ))}
        <div ref={bottomRef} />
      </div>
      <div className="flex gap-2">
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => e.key === "Enter" && sendMessage()}
          placeholder="Type a message..."
          className="flex-1 border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-400"
        />
        <button
          onClick={sendMessage}
          disabled={isLoading || !input.trim()}
          className="bg-blue-500 text-white px-4 py-2 rounded-lg disabled:opacity-50"
        >
          Send
        </button>
      </div>
    </main>
  );
}

Enter fullscreen mode Exit fullscreen mode

Two implementation details to highlight. First, the line !== "data: [DONE]" check: without it, the loop tries to parse [DONE] as JSON and throws an error. Second, the functional setMessages((prev) => ...) update: inside an async loop, closures capture stale state. Using prev ensures you're always appending to the latest message content.

Step 5: Build and Run

npm run dev
# ▲ Next.js 16.2.6 (Turbopack)
# ✓ Ready in 337ms
# Local: http://localhost:3000

npm run build
# ✓ Compiled successfully in 1874ms
# ƒ /api/chat  (Dynamic)

Enter fullscreen mode Exit fullscreen mode

337ms dev server startup is noticeably faster than Webpack-based builds. The production build also runs TypeScript checks automatically — type errors fail the build, which is the right behavior for a typed codebase.

Limitations of This Implementation

I'll be straight: don't ship this to production as-is.

No error handling. When the Claude API fails — rate limit, network error, invalid key — the stream just drops. The user sees nothing. Real services need try/catch blocks and error SSE events:

// Route Handler with error handling
export async function POST(req: Request) {
  try {
    const { messages } = await req.json();
    const stream = await client.messages.stream({ /* ... */ });
    const readable = new ReadableStream({
      async start(controller) {
        try {
          for await (const chunk of stream) { /* ... */ }
        } catch (streamError) {
          controller.enqueue(
            encoder.encode(`data: ${JSON.stringify({ error: "Stream error" })}\n\n`)
          );
        } finally {
          controller.enqueue(encoder.encode("data: [DONE]\n\n"));
          controller.close();
        }
      },
    });
    return new Response(readable, { /* headers */ });
  } catch (err) {
    return new Response(JSON.stringify({ error: "Request failed" }), {
      status: 500,
      headers: { "Content-Type": "application/json" },
    });
  }
}

Enter fullscreen mode Exit fullscreen mode

No conversation length limit. The full message history goes to the API on every request. Long conversations will eventually exceed the context window or drive up costs. Production apps need to trim to the last N messages or manage token counts.

No concurrent request management. Rapid messages or multiple tabs cause streaming collisions. AbortController logic for canceling previous requests is missing.

Deploying to Vercel

A few things I hit when deploying:

Environment variables: Add ANTHROPIC_API_KEY in Vercel's Project Settings → Environment Variables. The .env.local file stays local.

Runtime: Explicitly set Node.js Runtime in your Route Handler to avoid Edge Runtime compatibility issues:

export const runtime = 'nodejs';

Enter fullscreen mode Exit fullscreen mode

Function timeout: Vercel Hobby plan has a 10-second limit. For longer responses, add this to vercel.json:

{
  "functions": {
    "src/app/api/chat/route.ts": {
      "maxDuration": 60
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

How SSE Works Under the Hood

Server-Sent Events is a one-way streaming protocol running over plain HTTP. Unlike WebSockets, it passes through proxies, CDNs, and firewalls with no special handling.

SSE message format:

data: {"text": "Hello"}\n\n
data: {"text": ", World"}\n\n
data: [DONE]\n\n

Enter fullscreen mode Exit fullscreen mode

Each message starts with data: and ends with two newlines. The TextEncoder/TextDecoder pair converts between strings and Uint8Array (bytes) — Web Streams API operates at the byte level. This same pattern works across Next.js, Cloudflare Workers, Deno, and Bun.

Raw API vs. Vercel AI SDK

Aspect Raw Anthropic SDK Vercel AI SDK
Code volume More (~50-line Route Handler) Less (useChat one-liner)
Customization Completely free Within SDK abstractions
Debuggability SSE flow is transparent Internal logic is opaque
Learning value Forces you to understand Web Streams and SSE Use immediately
Best for Understanding streaming mechanics Rapid prototyping

My recommendation: build it the raw way once, then use the SDK. You'll understand what the SDK is actually handling for you. See Building a Claude Streaming Agent with Vercel AI SDK for the SDK-based comparison.

Next Steps

  1. Add Tool Use — Give Claude function-calling ability → Claude Agent SDK Complete Guide
  2. Prompt Caching — Cut API costs up to 90% → Claude API Prompt Caching in Practice
  3. Stronger error handling — AbortController, retry logic, error SSE events
  4. Stream cancellation — Cancel button to stop generation mid-stream
  5. Vercel deployment — Apply the notes above and go live

A deeper guide covering those production gaps is coming in a follow-up post.