Your client can call your server code directly, so you do not want to write an HTTP route for chat. Hand that function to useChat. Pick the helper from what your function returns:
| Your function | Use |
|---|---|
| Returns an AsyncIterable<StreamChunk> synchronously (in-process chat(), an RSC stream, tests) | stream() |
| Returns a Promise, such as a TanStack Start server function | fetcher |
| Is an RPC method that returns an async iterable | rpcStream() |
The choice depends on whether your function is sync or async. Both stream() and fetcher can yield an AsyncIterable<StreamChunk>. A server function returns a Promise, so it does not type-check in stream(). All three become the same request-scoped adapter. So stop(), abort, errors, and tool calls work the same.
stream() takes a factory that returns an AsyncIterable<StreamChunk> synchronously. It connects that iterable straight to the client:
import { useChat, stream } from "@tanstack/ai-react";
import { chatServerFn } from "./server/chat.server";
// `chatServerFn` is an in-process server-side function that synchronously
// returns an AsyncIterable<StreamChunk>, for example the result of
// `chat()` on the server.
const { messages } = useChat({
connection: stream((messages, data) => chatServerFn({ messages, ...data })),
});The factory gets the conversation messages and the per-request data that you passed to sendMessage. Return any async iterable that yields StreamChunk objects: a generator, the output of chat(), or a transformed stream.
stream() is request-scoped. For each sendMessage, it calls the factory once, runs the iterable to its end, and closes the connection. For one long-lived channel that carries many sends, see Custom Transports.
stream() takes an optional second argument with persistence handlers. These let server-driven persistence (persistence: true) work without an HTTP endpoint. Each one is usually a one-line call into your server:
For the full wiring, see Generation Persistence.
A TanStack Start server function always returns a Promise. Pass it as the top-level fetcher option, not as a connection. Give useChat exactly one of fetcher or connection. The option works like fetcher on the generation hooks.
The most common server function ends with toServerSentEventsResponse(...) and resolves to a Response:
// server/chat.server.ts
import { createServerFn } from "@tanstack/react-start";
import { chat, toServerSentEventsResponse } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import type { UIMessage } from "@tanstack/ai";
export const chatFn = createServerFn({ method: "POST" })
.inputValidator((data: { messages: Array<UIMessage> }) => data)
.handler(({ data }) =>
toServerSentEventsResponse(
chat({ adapter: openaiText("gpt-5.6"), messages: data.messages }),
),
);import { useChat } from "@tanstack/ai-react";
import { chatFn } from "./server/chat.server";
const { messages, sendMessage } = useChat({
fetcher: ({ messages }, { signal }) => chatFn({ data: { messages }, signal }),
});The fetcher gets { messages, data, threadId, runId } and an AbortSignal. The signal fires on stop(), or when a new send replaces the active one. Return one of these:
The fetcher can return the value directly or in a Promise.
Tip: The generation hooks (useGenerateImage and the others) also accept hydrateGeneration and joinRun options next to their fetcher. So persistence: true hydrates and rejoins through server functions, with no HTTP route. See Generation Persistence: Server functions / direct.
rpcStream() works the same as stream(). The name reads better when you call an RPC client. Use it with Cap'n Web, gRPC-Web, tRPC subscriptions, or any RPC framework that returns an async iterable:
import { useChat, rpcStream } from "@tanstack/ai-react";
import { api } from "./rpc-client";
// `api.chat.stream` is your RPC method. It must return an AsyncIterable<StreamChunk>.
const { messages } = useChat({
connection: rpcStream((messages, data) =>
api.chat.stream({ messages, ...data }),
),
});Like stream(), rpcStream() takes an optional second argument with persistence handlers ({ hydrate, hydrateGeneration, joinRun }). Then server-driven persistence works over RPC. Each handler is usually a one-line RPC call.
Call sendMessage("Hello"). The reply streams into messages, and your app has no chat route of its own.