Quick Start
This guide adds a chat that renders tokens while the model writes them. You add a server route, an API key, and a UI.
TanStack AI gives you each piece. chat() streams from your server route. A hook for your framework renders the tokens. This takes about 5 minutes.
Not on the web?
- React Native or Expo: go to Quick Start: React Native.
- No UI, only a backend: go to Quick Start: Server Only.
1. Install#
npm i @tanstack/ai @tanstack/ai-react @tanstack/ai-openaipnpm add @tanstack/ai @tanstack/ai-react @tanstack/ai-openaiyarn add @tanstack/ai @tanstack/ai-react @tanstack/ai-openaibun add @tanstack/ai @tanstack/ai-react @tanstack/ai-openaiPick your framework at the top of the page. The install command and the UI code follow your pick.
Tip
If you do not want a key for each provider, use OpenRouter. It gives you 300+ models with one API key.
2. Stream from the server#
Put your API key in the server environment, for example in .env:
OPENAI_API_KEY=your-openai-api-keyCreate a route at /api/chat. In Next.js, that is app/api/chat/route.ts. Call chat(), then return the stream with toServerSentEventsResponse:
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-6.1-sol"),
messages,
threadId,
runId,
});
return toServerSentEventsResponse(stream);
}The adapter reads OPENAI_API_KEY at runtime. Do not send this key to the browser.
3. Render the chat#
Call useChat from @tanstack/ai-react. Hold the composer text in useState. Pass it to sendMessage.
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"),
threadId: "support-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.
No framework? Use ChatClient from @tanstack/ai-client. See the client API.
4. Try it#
Start your app and send a message. The answer appears word by word while the model writes it. Click Stop to cancel a run.
You now have a streaming chat.
How it works#
- sendMessage posts the messages to /api/chat.
- chatParamsFromRequest reads the messages and the run ids from the request.
- chat() calls the model through the adapter and returns a stream of events.
- toServerSentEventsResponse sends those events back as Server-Sent Events.
- fetchServerSentEvents reads the events, and the hook adds each chunk to messages.
Each message has parts. This page renders only the text parts. Tool calls and other content arrive as other part types.
Next#
Add one feature at a time:
- Tools: let the model call your functions.
- Streaming: cancel, callbacks, and transports.
- Adapters: switch to another provider.
Or build a full app in a tutorial, such as Basic Chat or Generate Image.
Advanced#
Hosts#
The server route returns a Web Response. It works with any host that can return one:
- TanStack Start
- Next.js
- SvelteKit
- Hono
- A Remix controller action
If your server uses Node streams (Express), see Quick Start: Server Only.
Bad request bodies#
If the request body is not valid, chatParamsFromRequest throws a Response with status 400.
- TanStack Start, SolidStart, Remix, and React Router 7 return a thrown Response to the client for you.
- Next.js Route Handlers, SvelteKit, Hono, and raw Node do not. Wrap the call in try/catch and return the caught Response.
Keys in the browser#
If you do not want a key on the server, users can paste their own provider key. See Bring Your Own Key.