Module P-18·26 min read

ReadableStream and TransformStream in Route Handlers, the Vercel AI SDK (streamText, useChat, useCompletion), token-by-token streaming to the browser, abort signal propagation for cancelled requests, rate limiting streaming endpoints, streaming error handling constraints, and cost control via token budgets.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

P-18 — AI Integration and Streaming Route Handlers

Who this is for: Engineers who've built Route Handlers before (F-7) and now need to wire an LLM into one — a chat endpoint, a completion box, a "summarize this" button — and want the response to appear token-by-token instead of as one long spinner. This module covers the raw streaming primitives underneath, the Vercel AI SDK that wraps them, and the operational concerns that only show up once real users start hammering an endpoint that costs money per request: cancellation, rate limiting, in-stream error handling, and token budgets.


Why Streaming Matters Here More Than Anywhere Else

You've already seen Route Handlers return a ReadableStream for Server-Sent Events in F-7. AI text generation is the other major reason to reach for streaming, and it's a more urgent one. A non-streamed call to a large language model can easily take 5–20 seconds to produce a few paragraphs — the model is generating output token by token internally regardless, you're just choosing whether to buffer all of it before sending anything back, or to forward each piece as it's produced.

For a chat interface, buffering means the user stares at a blank screen for the full duration, then the whole answer appears at once. Streaming means the first words appear within a second and the rest arrives progressively, which is a dramatically different perceived-performance experience for identical total latency. This module is entirely about the mechanics of getting tokens from a model provider's response to the browser as they're generated, and about not blowing up your AI provider bill or your server while doing it.

One thing to be clear about up front, because it resurfaces later: streaming changes when the user sees output, not how much output costs. The model still generates and bills for the same number of tokens whether you stream them or send them all at once. Keep that distinction in mind — it matters a lot for the cost-control section near the end.


The Raw Primitive: Returning a ReadableStream

Before reaching for a library, it's worth seeing the raw mechanism, because the library is just a well-built wrapper around it. A Route Handler can return a ReadableStream directly as the body of a Response:

ts

This is the entire mechanism. ReadableStream is a standard Web API, not a Next.js-specific construct — it works because Route Handlers speak the same Request/Response interfaces as the rest of the web platform. Every chunk you enqueue() is flushed to the client as soon as it's written, rather than waiting for controller.close().

When you swap setTimeout and a hardcoded word list for an actual model provider call, this is conceptually what token-by-token streaming is: a loop that receives tokens from the provider's stream and re-enqueues them onto the response stream as they arrive. In practice you won't write that loop by hand for a production chat feature — the AI SDK does it — but understanding that it's "just" a ReadableStream demystifies a lot of what the library does under the hood, and it's useful when you need to debug a stream that isn't behaving, or build something the SDK doesn't directly support.


TransformStream — Reshaping Chunks in Flight

Sometimes you don't want to just pass raw provider chunks straight through — you want to annotate them, filter them, or reformat them before they reach the client. TransformStream, also a standard Web API, sits between a readable source and your response and lets you rewrite each chunk as it passes:

ts

pipeThrough() chains the source stream into the transform, and the result is itself a ReadableStream you hand to Response exactly as before. This pattern is what you'd use if, say, you wanted to inject a [DONE] sentinel, wrap raw text chunks into a JSON envelope for the client to parse consistently, or count tokens as they pass through for logging without buffering the whole response. The AI SDK's response helpers do something structurally similar internally — this is what's happening one layer down.

You won't usually need to write this by hand for a standard chat endpoint. Reach for it when you need custom framing around a provider's raw stream that the SDK's built-in response helpers don't already give you.


The Vercel AI SDK — Server Side: streamText

Writing the token relay loop by hand for every provider (OpenAI, Anthropic, and others each have their own streaming response shape) gets old fast, and it's exactly the kind of undifferentiated plumbing a library should own. The ai package (the Vercel AI SDK) provides streamText() as the core server-side primitive: you give it a model and a prompt or message history, and it returns a stream you can turn directly into a Response.

ts

A few things worth being precise about here, because the AI SDK has moved fast across major versions and this is exactly the kind of detail that goes stale:

  • The provider packages (@ai-sdk/openai, @ai-sdk/anthropic, and so on) are separate installs from the core ai package — the SDK deliberately splits "the streaming/orchestration layer" from "the model provider adapters."
  • The exact response-conversion method name (toDataStreamResponse() here) and the exact shape of the streamed protocol have changed between AI SDK major versions. Treat the method name above as illustrative of the pattern — a server function that turns a streamText() result into a Response your Route Handler can return — and verify the current method name against the version of ai in your package.json before shipping. This is not a detail worth memorizing across versions; it's a detail worth looking up each time you upgrade.
  • streamText() itself does the actual work of calling the provider's streaming API and re-exposing the output as an async-iterable/stream you can consume multiple ways (as a Response, as an async iterator of text deltas, or as a raw ReadableStream) depending on what the current SDK version exposes on the result object.

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.