TanStack
Getting Started

Quick Start

You want a streaming chat in your app. TanStack AI streams from a server route. The hook for your framework renders the tokens.

Tip

If you do not want a key per provider, OpenRouter gives you 300+ models with one API key.

React Native or Expo needs an absolute server URL and an XHR transport. See Quick Start: React Native.

No UI: see Quick Start: Server Only.

1. Install

shell
npm i @tanstack/ai @tanstack/ai-react @tanstack/ai-openai

2. Stream from the server

Call chat(). Then wrap the result with toServerSentEventsResponse.

ts
import {
  chat,
  chatParamsFromRequest,
  toServerSentEventsResponse,
} from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";

export async function POST(request: Request) {
  const { messages, threadId, runId } = await chatParamsFromRequest(request);

  const stream = chat({
    adapter: openaiText("gpt-5.6"),
    messages,
    threadId,
    runId,
  });

  return toServerSentEventsResponse(stream);
}

This works with TanStack Start, Next.js, SvelteKit, Hono, and any host that returns a Web Response.

If your server is Node streams (Express), see Quick Start: Server Only.

Put the API key on the server:

shell
OPENAI_API_KEY=your-openai-api-key

The adapter reads OPENAI_API_KEY at runtime. Do not send this key to the browser.

If you do not want a server key, see Bring Your Own Key.

3. Render the chat

Call useChat from @tanstack/ai-react. Hold the composer text in useState. Pass it to sendMessage.

tsx
import { useState } from "react";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";

export function Chat() {
  const [input, setInput] = useState("");
  const { messages, sendMessage, isLoading, stop } = useChat({
    connection: fetchServerSentEvents("/api/chat"),
  });

  return (
    <>
      {messages.map((message) => (
        <div key={message.id}>
          {message.parts.map((part, index) =>
            part.type === "text" ? <p key={index}>{part.content}</p> : null,
          )}
        </div>
      ))}
      <form
        onSubmit={(event) => {
          event.preventDefault();
          if (input.trim() === "") {
            return;
          }
          sendMessage(input);
          setInput("");
        }}
      >
        <input
          value={input}
          onChange={(event) => setInput(event.target.value)}
        />
        {isLoading ? (
          <button type="button" onClick={stop}>
            Stop
          </button>
        ) : (
          <button type="submit">Send</button>
        )}
      </form>
    </>
  );
}

messages updates as chunks arrive. isLoading is true while the run is in flight.

See the React API.

Send a message. Tokens show up in the UI.

Later