Next.js 15 fullstack (App Router + Server Actions) · Lektion

Strömmande svar och ReadableStream i hanterare

Returnera data stegvis med ReadableStream för AI-token, loggar och progressiva nyttolaster.

Lektion 3 av 413 steg

Strömmande svar och ReadableStream i hanterare är en gratis lektion i Next.js 15 fullstack (App Router + Server Actions) på CoddyKit. Detta är lektion 3 av 4. Du kan läsa vilka 3 lektioner som helst i den här lärvägen kostnadsfritt i sin helhet – därefter låser CoddyKit PRO upp alla lektioner, plus praktisk övning med en inbyggd kodredigerare och en AI-lärare dygnet runt. Den ingår i lärvägen för Next.js 15 fullstack (App Router + Server Actions), och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i Next.js 15 fullstack (App Router + Server Actions) innehåller totalt 4 lektioner.

Varför strömma ett svar?

En vanlig Route Handler bygger hela hela body-innehållet i minnet och skickar sedan allt på en gång. För AI-tokenutdata, live-loggar eller stora rapporter innebär det att användaren stirrar på en tom skärm ända till slutet.

Strömning vänder på detta: du skickar delar till klienten så snart de blir tillgängliga. Webbläsaren börjar rendera de första bytena direkt, tiden till första byte minskar och du behöver aldrig hålla hela nyttolasten i RAM.

  • ReadableStream är den primitiva Web Standard-komponent som Next.js 15 använder för detta.
  • Du returnerar den direkt från en Route Handler inuti en Response.
  • Det fungerar både i Node.js- och Edge-runtime-miljöerna.

Strukturen hos ReadableStream

En ReadableStream skapas med ett objekt som innehåller metoden start(controller). Inuti den anropar du controller.enqueue(chunk) för att skicka data och controller.close() när du är klar.

Chunken bör vara en Uint8Array med byte. En TextEncoder omvandlar en sträng till sådana byte. Detta är ren Web API-kod, så den körs överallt där det finns en modern JS-motor.

const encoder = new TextEncoder();

const stream = new ReadableStream({
  start(controller) {
    controller.enqueue(encoder.encode("Hello, "));
    controller.enqueue(encoder.encode("streamed "));
    controller.enqueue(encoder.encode("world!"));
    controller.close();
  },
});

const response = new Response(stream);
console.log("Stream and Response created:", response instanceof Response);

Returnera en stream från en handler

I app/api/.../route.ts exporterar du en HTTP-metodfunktion (här GET) och returnerar en Response vars body är streamen.

Ange alltid Content-Type. För vanlig inkrementell text fungerar text/plain bra; för strukturerade eventströmmar använder du text/event-stream (detta behandlas senare).

Detta är framework-kod som behöver Next.js-servern och kan därför inte köras fristående.

// app/api/hello/route.ts
export async function GET(): Promise<Response> {
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    start(controller) {
      controller.enqueue(encoder.encode("chunk-1\n"));
      controller.enqueue(encoder.encode("chunk-2\n"));
      controller.close();
    },
  });

  return new Response(stream, {
    headers: { "Content-Type": "text/plain; charset=utf-8" },
  });
}

Strömma över tid med async start

Den verkliga kraften märks när chunks anländer över tid. Metoden start kan vara async, så att du kan använda await mellan anropen till enqueue.

Här simulerar en kort fördröjning arbete (en AI-leverantör, en långsam fråga eller ett jobbeg). Varje rad når klienten så snart den läggs i kön, inte när loopen är klar.

  • Använd await för att styra takten på utdata utan att blockera händelseloopen.
  • Glöm aldrig controller.close(), annars förblir anslutningen öppen.
const encoder = new TextEncoder();
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

const stream = new ReadableStream({
  async start(controller) {
    for (let i = 1; i <= 3; i++) {
      await sleep(50);
      controller.enqueue(encoder.encode(`step ${i}\n`));
    }
    controller.close();
  },
});

const reader = stream.getReader();
const decoder = new TextDecoder();
let out = "";
let result = await reader.read();
while (!result.done) {
  out += decoder.decode(result.value);
  result = await reader.read();
}
console.log(out.trim());

Strömma AI-token

Det klassiska användningsfallet: skicka en LLM:s tokenström direkt till webbläsaren så att texten visas ord för ord. De flesta AI-SDK:er tillhandahåller en async iterable med partiella chunks.

Iterera över den inuti start och lägg textdelta:t för varje token i kön. Användaren ser svaret byggas upp i realtid, precis som i ett chattgränssnitt.

// app/api/chat/route.ts
import { openai } from "@/lib/openai";

export async function POST(req: Request): Promise<Response> {
  const { prompt } = await req.json();
  const encoder = new TextEncoder();

  const completion = await openai.chat.completions.create({
    model: "gpt-4o-mini",
    stream: true,
    messages: [{ role: "user", content: prompt }],
  });

  const stream = new ReadableStream({
    async start(controller) {
      for await (const part of completion) {
        const token = part.choices[0]?.delta?.content ?? "";
        if (token) controller.enqueue(encoder.encode(token));
      }
      controller.close();
    },
  });

  return new Response(stream, {
    headers: { "Content-Type": "text/plain; charset=utf-8" },
  });
}

Server-Sent Events-format (SSE)

För strukturerade händelser med namn, som webbläsarens EventSource förstår, använder du SSE-formatet på wire-nivå och innehållstypen text/event-stream.

Varje meddelande är en rad som börjar med data: följt av en nyttolast och avslutas med en dubbel radbrytning (\n\n). Du kan serialisera JSON efter data:.

  • Content-Type: text/event-stream
  • Cache-Control: no-cache så att proxyer inte buffrar.
  • Connection: keep-alive i Node-runtime-miljön.
// app/api/events/route.ts
export async function GET(): Promise<Response> {
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      for (const status of ["queued", "running", "done"]) {
        const payload = JSON.stringify({ status });
        controller.enqueue(encoder.encode(`data: ${payload}\n\n`));
        await new Promise((r) => setTimeout(r, 300));
      }
      controller.close();
    },
  });

  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  });
}

Strömma förloppsloggar

Långvariga jobb (importer, byggen och batchbearbetning) har nytta av att strömma en förloppslogg. Varje slutfört steg läggs i kön, så att klienten kan uppdatera en livekonsol utan att polla.

Mönstret är identiskt: utför arbete, lägg en rad i kön och upprepa. Nedan finns en körbar simulering av ett jobb i flera steg som skickar NDJSON (ett JSON-objekt per rad).

const encoder = new TextEncoder();
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
const steps = ["fetch", "transform", "upload"];

const stream = new ReadableStream({
  async start(controller) {
    for (let i = 0; i < steps.length; i++) {
      await sleep(30);
      const line = JSON.stringify({ step: steps[i], pct: ((i + 1) / steps.length) * 100 });
      controller.enqueue(encoder.encode(line + "\n"));
    }
    controller.close();
  },
});

const reader = stream.getReader();
const decoder = new TextDecoder();
let buf = "";
let r = await reader.read();
while (!r.done) {
  buf += decoder.decode(r.value);
  r = await reader.read();
}
for (const l of buf.trim().split("\n")) console.log(JSON.parse(l).step);

Hantera frånkopplingar från klienten

Om användaren stänger fliken mitt under strömningen bör du sluta arbeta. Request innehåller en AbortSignal i req.signal som aktiveras när anslutningen bryts.

Kontrollera req.signal.aborted i loopen och använd eventuellt streamens callback cancel() för att frigöra resurser (stäng en LLM-anslutning eller avbryt en databaskursor).

// app/api/long/route.ts
export async function GET(req: Request): Promise<Response> {
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      for (let i = 0; i < 100; i++) {
        if (req.signal.aborted) break; // client left
        controller.enqueue(encoder.encode(`tick ${i}\n`));
        await new Promise((r) => setTimeout(r, 200));
      }
      controller.close();
    },
    cancel(reason) {
      console.log("stream cancelled:", reason);
    },
  });

  return new Response(stream, {
    headers: { "Content-Type": "text/plain; charset=utf-8" },
  });
}

Felhantering i en stream

När du väl har returnerat Response är HTTP-statusen redan 200 och headers har skickats. Du kan inte byta till 500 mitt under strömningen.

Omslut därför riskfyllt arbete med try/catch och rapportera fel som en chunk (till exempel en SSE-rad med event: error eller ett JSON-felobjekt), och stäng sedan streamen. Använd controller.error(e) endast när du vill avsluta streamen abrupt.

// app/api/job/route.ts
export async function GET(): Promise<Response> {
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      try {
        const data = await riskyWork();
        controller.enqueue(encoder.encode(JSON.stringify(data) + "\n"));
      } catch (err) {
        const msg = err instanceof Error ? err.message : "unknown";
        controller.enqueue(encoder.encode(JSON.stringify({ error: msg }) + "\n"));
      } finally {
        controller.close();
      }
    },
  });

  return new Response(stream, {
    headers: { "Content-Type": "application/x-ndjson" },
  });
}

Edge-runtime-miljön och backpressure

Strömning fungerar särskilt bra i Edge-runtime-miljön. Aktivera den med export const runtime = "edge". Edge-runtime-miljön bygger på Web Streams, så samma ReadableStream-kod fungerar oförändrad och börjar skickas direkt från en plats nära användaren.

Backpressure: om klienten läser långsamt buffrar controller.enqueue fortfarande data. För producenter med hög volym bör du föredra en pull-baserad källa eller kontrollera controller.desiredSize för att anpassa takten och undvika obegränsad minnestillväxt.

// app/api/edge-stream/route.ts
export const runtime = "edge";

export async function GET(): Promise<Response> {
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      for (let i = 0; i < 5; i++) {
        // desiredSize < 0 means the consumer is behind
        if ((controller.desiredSize ?? 1) > 0) {
          controller.enqueue(encoder.encode(`edge ${i}\n`));
        }
        await new Promise((r) => setTimeout(r, 100));
      }
      controller.close();
    },
  });

  return new Response(stream, {
    headers: { "Content-Type": "text/plain; charset=utf-8" },
  });
}

Ta emot streamen på klienten

I webbläsaren ger fetch dig response.body, som själv är en ReadableStream. Läs den med en reader och avkoda chunks när de anländer, så att gränssnittet uppdateras stegvis.

För SSE kan du i stället använda det inbyggda API:t EventSource. För råtext eller NDJSON är reader-loopen nedan den generella metoden.

// components/StreamReader.ts
export async function readStream(url: string, onChunk: (text: string) => void) {
  const res = await fetch(url);
  if (!res.body) throw new Error("No response body to stream");

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

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    onChunk(decoder.decode(value, { stream: true }));
  }
}

Snabbkontroll

Testa dina kunskaper om Route Handlers med strömning.

Sammanfattning

Du har lärt dig att strömma inkrementella data från Next.js 15 Route Handlers:

  • ReadableStream med start(controller) samt controller.enqueue() / controller.close() är den centrala primitiva komponenten; returnera den inuti en Response.
  • Med en async start kan du invänta mellan chunks för AI-token, förloppsloggar och SSE-händelser.
  • Använd text/event-stream + data: ...\n\n för SSE eller NDJSON för radavgränsad JSON.
  • Håll koll på req.signal.aborted vid frånkopplingar och städa upp i cancel().
  • Status och headers är fasta när du väl har returnerat, så rapportera fel mitt under strömningen som chunks.
  • Edge-runtime-miljön (runtime = "edge") kör samma Web Streams-kod; tänk på backpressure via desiredSize.
Gratis att börja

Lär dig TypeScript med en AI-lärare – gratis

Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.

Kurser
22
Lektioner
88

Vanliga frågor

Är lektionen ”Strömmande svar och ReadableStream i hanterare” gratis?

Ja – du kan läsa vilka 3 lektioner som helst i lärvägen Next.js 15 fullstack (App Router + Server Actions), inklusive ”Strömmande svar och ReadableStream i hanterare”, kostnadsfritt i sin helhet här på webben. Därefter låser CoddyKit PRO upp alla lektioner, plus interaktiv övning med en inbyggd kodredigerare och en AI-lärare dygnet runt. Kursen i Next.js 15 fullstack (App Router + Server Actions) innehåller totalt 4 lektioner.

Vad lär jag mig i ”Strömmande svar och ReadableStream i hanterare”?

Returnera data stegvis med ReadableStream för AI-token, loggar och progressiva nyttolaster. Ni övar på Next.js 15 fullstack (App Router + Server Actions) med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.

Behöver jag någon erfarenhet för att börja lära mig Next.js 15 fullstack (App Router + Server Actions)?

Du behöver inga förkunskaper. Utbildningen i Next.js 15 fullstack (App Router + Server Actions) på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 3 av 4.

Hur lång tid tar lektionen ”Strömmande svar och ReadableStream i hanterare”?

De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.

Kan jag skriva och köra kod i den här Next.js 15 fullstack (App Router + Server Actions)-lektionen?

Ja. Varje Next.js 15 fullstack (App Router + Server Actions)-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.

Alla lektioner i den här kursen

  1. Utforma RESTful-routningshanterare med Web Request API
  2. Avvägningar mellan Node-runtime och Edge-runtime
  3. Strömmande svar och ReadableStream i hanterare
  4. Validering av begäranden och typade JSON-svar med Zod
← Tillbaka till Next.js 15 fullstack (App Router + Server Actions)