C.W.K.
Stream
Lesson 02 of 06 · published

Streaming on Node and Next.js

~16 min · streaming, next-js, route-handler

Level 0Observer
0 XP0/64 lessons0/13 achievements
0/150 XP to next level150 XP to go0% complete

The async iterator

The TypeScript SDK exposes streaming as client.messages.stream(...) returning an async iterator of typed events plus a finalMessage() promise. Iterate with for await; await finalMessage for usage. Same shape as Python, just typed.

Next.js Route Handlers and ReadableStream

Next.js App Router Route Handlers can return a Response with a ReadableStream body. Pipe the SDK's text stream into the Response stream and the browser sees tokens as they arrive. cwk-site uses this pattern; the same shape works on Vercel Edge.

Backpressure and cancellation

If the client disconnects mid-stream, you want to stop generating to save tokens. Wire the request's AbortSignal to the SDK call so server-side generation halts when the browser tab closes. The SDK accepts signal in every method.

Principle: Streaming is transport plumbing. Hook the same abort signal you would use for any HTTP work, and let the SDK do the rest.

Code

Stream iteration with typed events·typescript
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();

const stream = await client.messages.stream({
  model: 'claude-sonnet-4-6',
  max_tokens: 1024,
  messages: [{ role: 'user', content: 'Three streaming tips.' }],
});

for await (const event of stream) {
  if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
    process.stdout.write(event.delta.text);
  }
}
const final = await stream.finalMessage();
console.log('\nusage:', final.usage);
Next.js App Router streaming Route Handler·typescript
// app/api/chat/route.ts (Next.js 16, App Router)
import Anthropic from '@anthropic-ai/sdk';

export const runtime = 'nodejs'; // or 'edge' if you tested it

const client = new Anthropic();

export async function POST(req: Request) {
  const { user } = await req.json();

  const stream = new ReadableStream<Uint8Array>({
    async start(controller) {
      const enc = new TextEncoder();
      const upstream = await client.messages.stream({
        model: 'claude-sonnet-4-6',
        max_tokens: 2048,
        messages: [{ role: 'user', content: user }],
        signal: req.signal, // cancel upstream if browser disconnects
      });
      try {
        for await (const event of upstream) {
          if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
            controller.enqueue(enc.encode(event.delta.text));
          }
        }
      } finally {
        controller.close();
      }
    },
  });

  return new Response(stream, {
    headers: { 'content-type': 'text/plain; charset=utf-8' },
  });
}

External links

Exercise

Build a Next.js Route Handler that streams a Claude response to the client and aborts upstream when the request is canceled. Verify cancellation by closing the browser tab mid-response and watching server logs for the abort.
Hint
If you do not see upstream abort, you forgot to pass req.signal into messages.stream().

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.