---
title: "Streaming Summaries"
description: "Replace blocking generateText with streamText for real-time AI responses. Users see content appear word-by-word instead of waiting for the full response."
canonical_url: "https://vercel.com/academy/ai-summary-app-with-nextjs/streaming-summaries"
md_url: "https://vercel.com/academy/ai-summary-app-with-nextjs/streaming-summaries.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-04-11T10:58:19.381Z"
content_type: "lesson"
course: "ai-summary-app-with-nextjs"
course_title: "Creating an AI Summary App with Next.js"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Streaming Summaries

# Streaming summaries

Is anyone in physical pain right now from watching the routes change? If the slowness is killing you, let's fix it with streaming.

## Outcome

Replace `generateText` with `streamText` to stream AI summaries in real-time, showing users content as it's generated.

## Fast Track

1. Update `lib/ai-summary.ts`: change `generateText` to `streamText`, return `result.textStream`
2. Convert `AIReviewSummary` to Client Component with `"use client"`, use `useEffect` to consume the stream
3. Test at `/mower`—summary text appears word-by-word instead of all at once

## Hands-on Exercise 2.4

Add streaming to the AI summary feature:

**Requirements:**

1. Change `summarizeReviews` to return a stream instead of a string
2. Create a new async function that returns the stream object
3. Update `AIReviewSummary` to consume the stream with React state
4. Show a loading indicator while waiting for first chunk
5. Display text as it streams in

**Implementation hints:**

- `streamText` returns `{ textStream }` which is an async iterable
- Client Components can use `useState` and `useEffect` to handle streams
- Use Server Actions to call the streaming function from Client Components
- Consider showing "Generating summary..." before first chunk arrives

## Understanding streamText

The AI SDK provides `streamText` for streaming responses:

```typescript
import { streamText } from "ai";

const result = streamText({
  model: "anthropic/claude-sonnet-4-5",
  prompt: "Your instructions here",
});

// result.textStream is an async iterable
for await (const chunk of result.textStream) {
  console.log(chunk); // Each chunk as it arrives
}
```

**Key differences from generateText:**

- Returns immediately (doesn't wait for full response)
- `textStream` yields chunks as they're generated
- Better UX for longer responses

## Step 1: Create Streaming Function

Update `lib/ai-summary.ts` to add a streaming version:

```typescript title="lib/ai-summary.ts" {1,5-56}
import { generateText, streamText } from "ai";
import { Product } from "./types";

// Keep the existing summarizeReviews function for now
// Add this new streaming function

export async function streamReviewSummary(product: Product) {
  const averageRating =
    product.reviews.reduce((acc, review) => acc + review.stars, 0) /
    product.reviews.length;

  const prompt = `Write a summary of the reviews for the ${
    product.name
  } product. The product's average rating is ${averageRating} out of 5 stars.

Your goal is to highlight the most common themes and sentiments expressed by customers.
If multiple themes are present, try to capture the most important ones.
If no patterns emerge but there is a shared sentiment, capture that instead.
Try to use natural language and keep the summary concise.
Use a maximum of 4 sentences and 30 words.
Don't include any word count or character count.
No need to reference which reviews you're summarizing.
Do not reference the star rating in the summary.

Start the summary with "Customers like…" or "Customers mention…"

Here are 3 examples of good summaries:
Example 1: Customers like the quality, space, fit and value of the sport equipment bag case. They mention it's heavy duty, has lots of space and pockets, and can fit all their gear. They also appreciate the portability and appearance. That said, some disagree on the zipper.
Example 2: Customers like the quality, ease of installation, and value of the transport rack. They mention that it holds on to everything really well, and is reliable. Some complain about the wind noise, saying it makes a whistling noise at high speeds. Opinions are mixed on fit, and performance.
Example 3: Customers like the quality and value of the insulated water bottle. They say it keeps drinks cold for hours and the lid seals well. Some customers have different opinions on size and durability.

Hit the following tone based on rating:
- 1-2 stars: negative
- 3 stars: neutral
- 4-5 stars: positive

The customer reviews to summarize are as follows:
${product.reviews
    .map((review, i) => `Review ${i + 1}:\n${review.review}`)
    .join("\n\n")}`;

  const result = streamText({
    model: "anthropic/claude-sonnet-4-5",
    prompt,
    maxTokens: 1000,
    temperature: 0.75,
  });

  return result;
}
```

**What changed:**

- Added `streamText` import
- New `streamReviewSummary` function returns the stream result directly
- Same prompt as the engineered version from 2.3

## Step 2: Create Server Action

Create `app/actions/stream-summary.ts`:

```typescript title="app/actions/stream-summary.ts"
"use server";
 
import { streamReviewSummary } from "@/lib/ai-summary";
import { getProduct } from "@/lib/sample-data";
 
export async function getStreamingSummary(productSlug: string) {
  const product = getProduct(productSlug);
  const result = await streamReviewSummary(product);
  return result.toTextStreamResponse();
}
```

This Server Action wraps the streaming function and returns a response that can be consumed by the client.

## Step 3: Create Streaming Component

Create `components/streaming-summary.tsx`:

```tsx title="components/streaming-summary.tsx"
"use client";

import { useEffect, useState } from "react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { FiveStarRating } from "./five-star-rating";
import { Product } from "@/lib/types";

export function StreamingSummary({ product }: { product: Product }) {
  const [summary, setSummary] = useState("");
  const [isLoading, setIsLoading] = useState(true);

  const averageRating =
    product.reviews.reduce((acc, review) => acc + review.stars, 0) /
    product.reviews.length;

  useEffect(() => {
    async function fetchStream() {
      setIsLoading(true);
      setSummary("");

      try {
        const response = await fetch(`/api/summary/${product.slug}`);

        if (!response.ok) {
          throw new Error("Failed to fetch summary");
        }

        const reader = response.body?.getReader();
        const decoder = new TextDecoder();

        if (!reader) {
          throw new Error("No reader available");
        }

        setIsLoading(false);

        while (true) {
          const { done, value } = await reader.read();
          if (done) break;

          const chunk = decoder.decode(value, { stream: true });
          setSummary((prev) => prev + chunk);
        }
      } catch (error) {
        console.error("Stream error:", error);
        setSummary("Unable to generate summary. Please try again.");
        setIsLoading(false);
      }
    }

    fetchStream();
  }, [product.slug]);

  return (
    <Card className="w-full max-w-prose p-10 grid gap-10">
      <CardHeader className="items-center space-y-0 gap-4 p-0">
        <div className="grid gap-1 text-center">
          <CardTitle className="text-lg">AI Summary</CardTitle>
          <p className="text-xs text-muted-foreground">
            Based on {product.reviews.length} customer ratings
          </p>
        </div>
        <div className="bg-gray-100 px-3 rounded-full flex items-center py-2 dark:bg-gray-800">
          <FiveStarRating rating={Math.round(averageRating)} />
          <span className="text-sm ml-4 text-gray-500 dark:text-gray-400">
            {averageRating.toFixed(1)} out of 5
          </span>
        </div>
      </CardHeader>
      <CardContent className="p-0 grid gap-4">
        <p className="text-sm leading-loose text-gray-500 dark:text-gray-400 min-h-[4rem]">
          {isLoading ? (
            <span className="animate-pulse">Generating summary...</span>
          ) : (
            summary
          )}
        </p>
      </CardContent>
    </Card>
  );
}
```

**Key features:**

- `"use client"` directive for React hooks
- `useState` tracks the streaming text and loading state
- `useEffect` fetches and consumes the stream
- Shows "Generating summary..." while waiting for first chunk
- Appends each chunk as it arrives

## Step 4: Create API Route for Streaming

Create `app/api/summary/[slug]/route.ts`:

```typescript title="app/api/summary/[slug]/route.ts"
import { streamReviewSummary } from "@/lib/ai-summary";
import { getProduct } from "@/lib/sample-data";

export async function GET(
  request: Request,
  { params }: { params: Promise<{ slug: string }> }
) {
  const { slug } = await params;

  let product;
  try {
    product = getProduct(slug);
  } catch {
    return new Response("Product not found", { status: 404 });
  }

  const result = await streamReviewSummary(product);

  return result.toTextStreamResponse();
}
```

**What this does:**

- Creates a streaming endpoint at `/api/summary/[slug]`
- Calls the streaming function and returns a text stream response
- The client reads this stream chunk by chunk

## Step 5: Update Product Page

Update `app/[productId]/page.tsx` to use the streaming component:

```tsx title="app/[productId]/page.tsx" {5,31}
import { Metadata } from "next";
import { notFound } from "next/navigation";
import { getProduct, getProducts } from "@/lib/sample-data";
import { Reviews } from "@/components/reviews";
import { StreamingSummary } from "@/components/streaming-summary";

export default async function ProductPage({
  params,
}: {
  params: Promise<{ productId: string }>;
}) {
  const { productId } = await params;

  let product;
  try {
    product = getProduct(productId);
  } catch {
    notFound();
  }

  return (
    <main className="min-h-screen p-8">
      <div className="max-w-4xl mx-auto space-y-8">
        <div>
          <h1 className="text-4xl font-bold">{product.name}</h1>
          <p className="text-lg text-muted-foreground mt-2">
            {product.description}
          </p>
        </div>

        <StreamingSummary product={product} />

        <Reviews product={product} />
      </div>
    </main>
  );
}

// ... (generateStaticParams and generateMetadata remain the same)
```

## Try It

1. **Run your dev server:**
   ```bash
   pnpm dev
   ```

2. **Visit a product page:**
   ```
   http://localhost:3000/mower
   ```

3. **Watch the summary stream in:**
   - "Generating summary..." appears first
   - Text starts appearing word-by-word
   - Summary completes in 2-3 seconds
   - Much better UX than waiting for full response!

4. **Compare the experience:**
   - **Before (blocking):** Blank card → wait 2-3s → full text appears
   - **After (streaming):** Loading text → immediate first word → text flows in

5. **Test different products:**
   - `/ecoBright` - Watch it stream
   - `/aquaHeat` - Each product streams independently

## How Streaming Works

**Request flow:**

```
1. Page loads → StreamingSummary component mounts
2. useEffect triggers → fetches /api/summary/mower
3. API route calls streamReviewSummary(product)
4. streamText sends request to Claude
5. Claude generates tokens one at a time
6. Each token streams back through:
   Claude → AI Gateway → Your API → Client
7. Client appends each chunk to state
8. React re-renders with new text
9. User sees words appear progressively
```

**Why it feels faster:**

- Time to first byte: \~200ms (instead of waiting 2-3s)
- User sees progress immediately
- Perceived performance is much better
- Same total generation time, better UX

## Streaming vs Blocking Comparison

| Aspect                | generateText (Blocking)  | streamText (Streaming)         |
| --------------------- | ------------------------ | ------------------------------ |
| Time to first content | 2-3 seconds              | \~200ms                        |
| Total time            | 2-3 seconds              | 2-3 seconds                    |
| User experience       | Wait, then see all       | See progress immediately       |
| Implementation        | Simpler                  | Slightly more complex          |
| Best for              | Short responses, caching | Longer responses, real-time UX |

## When to Use Streaming

**Use streaming when:**

- Response takes >1 second to generate
- User is waiting and watching
- Content is being read (summaries, explanations)
- You want engaging, dynamic UX

**Use blocking when:**

- Response is very short
- Result will be cached
- Processing happens in background
- Structured output (generateObject doesn't stream content)

## Commit

```bash
git add lib/ai-summary.ts app/api/summary/\[slug\]/route.ts components/streaming-summary.tsx app/\[productId\]/page.tsx
git commit -m "feat(ai): add streaming summaries with streamText"
git push
```

## Done-When

- [ ] `streamReviewSummary` function created using `streamText`
- [ ] API route `/api/summary/[slug]` returns streaming response
- [ ] `StreamingSummary` Client Component consumes the stream
- [ ] Loading state shows "Generating summary..."
- [ ] Text appears word-by-word as it streams
- [ ] Product page uses streaming component
- [ ] Verified streaming works on all product pages

## What's Next

Streaming gives users immediate feedback for text summaries. In the next lesson, you'll use `generateObject` to extract structured data—pros, cons, and themes—with full type safety using Zod schemas. Note: `generateObject` returns complete objects, not streams, since partial structured data isn't useful.

***

**Sources:**

- [AI SDK streamText](https://sdk.vercel.ai/docs/reference/ai-sdk-core/stream-text)
- [Streaming in Next.js](https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming)
- [ReadableStream API](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
