# Streaming

Receive responses as ReadableStreams for SSE, large downloads, and progressive rendering.

## Overview

By default, Courier parses the response body as JSON or text. When a call asks for `stream: true`, Courier returns the raw `ReadableStream` from `response.body` instead of parsing it.

This is useful for Server-Sent Events (SSE), large file downloads, and any scenario where you want to process data incrementally rather than buffering the entire response.

## Defining stream endpoints

Streaming is chosen by the caller, not by the endpoint: the endpoint says where to go, the call says how the body should arrive. Declare the response as a `ReadableStream` and let callers ask for it:

```ts title="api/events.ts"
import { courier, defineGroup } from "@noverax/courier/define"

const { endpoint } = courier()

export const events = defineGroup({
  prefix: "events",
  endpoints: {
    subscribe: endpoint.get(":channel/stream").variables<{ channel: string }>().response<ReadableStream>(),
  },
})

export const exports = defineGroup({
  prefix: "exports",
  endpoints: {
    download: endpoint.get(":id/download").variables<{ id: string }>().response<ReadableStream>(),
  },
})
```

Ask for the stream at the call site:

```ts
const response = await caller.events.with({ stream: true }).subscribe({ channel: "builds" })
```

## What a streamed call resolves to

A streamed call resolves to the unread body — a `ReadableStream<Uint8Array>`. Ask for `.raw` as well and it resolves to the response around it, `{ status, headers, data }`, with the stream in `data`. Both shapes are the same in direct and proxy mode, and the types follow the option: `.with({ stream: true })` changes what the call is typed to return.

```ts title="app/exports/[id]/page.tsx"
import { caller } from "@/lib/caller"

export default async function ExportPage({ params }: { params: { id: string } }) {
  const body = await caller.exports.with({ stream: true }).download({ id: params.id })
  // body: ReadableStream<Uint8Array>

  const response = await caller.exports.raw.with({ stream: true }).download({ id: params.id })
  // response.status, response.headers.get("content-type"), response.data: ReadableStream<Uint8Array>
}
```

In proxy mode the stream is not buffered: data flows from your backend through the proxy to the browser as it arrives, with the content type, and with the cookies the backend set where the endpoint names `set-cookie`. It is served from your application's origin, so it also carries `Content-Security-Policy: default-src 'none'; sandbox` and `X-Content-Type-Options: nosniff`: a streamed URL opened as a page cannot run a script the backend sent as your application. A caller reading the stream with `fetch` is unaffected.

A response schema describes the value a caller reads, not the bytes, so it is not applied to a streamed call. That makes it a question of who asks. A server-side caller may stream any endpoint. A browser may not stream one that declares a response schema: the schema is what keeps the fields it strips away from the page, and a stream would hand over the backend's body before the schema has seen it. The proxy answers such a call with `400`. Leave the schema off an endpoint the browser is meant to stream, as the `ReadableStream` endpoints above do — `.response<ReadableStream>()` names a type and declares no schema.

## Consuming streams in client components

Use the standard `ReadableStream` API to consume streamed responses. The `getReader()` method gives you a reader that yields chunks as they arrive:

```ts title="components/event-stream.tsx"
"use client"

import { useEffect, useState } from "react"

import { caller } from "@/lib/caller"

export function EventStream({ channel }: { channel: string }) {
  const [events, setEvents] = useState<string[]>([])

  useEffect(() => {
    let cancelled = false

    async function consume() {
      const body = await caller.events.with({ stream: true }).subscribe({ channel })
      const reader = body.getReader()
      const decoder = new TextDecoder()

      while (!cancelled) {
        const { done, value } = await reader.read()
        if (done) break
        const text = decoder.decode(value, { stream: true })
        setEvents((prev) => [...prev, text])
      }

      reader.releaseLock()
    }

    consume()
    return () => { cancelled = true }
  }, [channel])

  return (
    <ul>
      {events.map((event, i) => (
        <li key={i}>{event}</li>
      ))}
    </ul>
  )
}
```

## SSE pattern

Server-Sent Events follow a specific text format (`event:` and `data:` fields separated by double newlines). Here's a complete SSE consumer with event parsing:

```ts title="components/sse-consumer.tsx"
"use client"

import { useEffect, useState } from "react"

import { caller } from "@/lib/caller"

type SSEEvent = { event: string; data: string }

function parseSSE(chunk: string): SSEEvent[] {
  return chunk
    .split("\n\n")
    .filter(Boolean)
    .map((block) => {
      const lines = block.split("\n")
      const event = lines.find((l) => l.startsWith("event:"))?.slice(6).trim() ?? "message"
      const data = lines.find((l) => l.startsWith("data:"))?.slice(5).trim() ?? ""
      return { event, data }
    })
}

export function SSEConsumer({ channel }: { channel: string }) {
  const [messages, setMessages] = useState<SSEEvent[]>([])

  useEffect(() => {
    let cancelled = false

    async function consume() {
      const body = await caller.events.with({ stream: true }).subscribe({ channel })
      const reader = body.getReader()
      const decoder = new TextDecoder()

      while (!cancelled) {
        const { done, value } = await reader.read()
        if (done) break
        const text = decoder.decode(value, { stream: true })
        const events = parseSSE(text)
        setMessages((prev) => [...prev, ...events])
      }

      reader.releaseLock()
    }

    consume()
    return () => { cancelled = true }
  }, [channel])

  return (
    <ul>
      {messages.map((msg, i) => (
        <li key={i}>
          <strong>{msg.event}:</strong> {msg.data}
        </li>
      ))}
    </ul>
  )
}
```

## Next steps

- [Raw Data Mode](/docs/advanced/raw-data) — access status codes and headers alongside response data
- [Options](/docs/advanced/options) — all per-request options, stream and timeout among them
- [Middleware](/docs/advanced/middleware) — intercept and transform requests with composable middleware
