Quick Start: Server Only
This guide calls a model from a Node.js server. It has no UI framework.
chat() runs on the server and returns a stream. You can read it to the end as text, or send it over HTTP. This takes about 5 minutes.
1. Install#
npm i @tanstack/ai @tanstack/ai-openaipnpm add @tanstack/ai @tanstack/ai-openaiyarn add @tanstack/ai @tanstack/ai-openaibun add @tanstack/ai @tanstack/ai-openaiTip
If you do not want a key for each provider, use OpenRouter. It gives you 300+ models with one API key.
2. Add your API key#
Create a .env file:
OPENAI_API_KEY=your-openai-api-keyThe adapter reads OPENAI_API_KEY at runtime. Do not send this key to the browser.
3. Get a reply#
Call chat(), then read the whole stream with streamToText:
import { chat, streamToText } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: [{ role: 'user', content: 'Hello!' }],
threadId: 'thread-1',
})
const { text } = await streamToText(stream)
console.log(text)Run the file. The reply prints to the terminal.
4. Serve it over HTTP#
This Express server streams the reply as Server-Sent Events on /api/chat:
import express from 'express'
import {
chat,
chatParamsFromRequestBody,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
const app = express()
app.use(express.json())
app.post('/api/chat', async (req, res) => {
const { messages, threadId, runId } = await chatParamsFromRequestBody(
req.body,
)
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages,
threadId,
runId,
})
const response = toServerSentEventsResponse(stream)
res.writeHead(response.status, Object.fromEntries(response.headers))
const body = response.body
if (body) {
const reader = body.getReader()
const pump = async () => {
const { done, value } = await reader.read()
if (done) {
res.end()
return
}
res.write(value)
await pump()
}
await pump()
}
})
app.listen(3000, () => console.log('Server running on port 3000'))The client hooks (useChat in @tanstack/ai-react, @tanstack/ai-vue, @tanstack/ai-svelte, and the others) can talk to this endpoint. Add a UI later with the Quick Start.
You now have a model that answers from your backend.
How it works#
- chat() returns an AsyncIterable<StreamChunk>. Nothing runs until you read it.
- streamToText reads the stream to the end. It returns { text, chunks }. text is the full reply. chunks holds the rest of the run: tool calls, usage, and interrupts.
- toServerSentEventsResponse turns the stream into a Web Response in the TanStack AI SSE format.
Any backend that returns this format works. Fastify, Hono, and other Node.js frameworks are fine.
Next#
- Tools: add function calling and agent loops.
- Adapters: connect to other providers.
- Quick Start: add a frontend.
Advanced#
Add a server tool#
Define a tool with toolDefinition, then pass it to chat(). The agent loop calls the tool and sends the result back to the model:
import { chat, toolDefinition, streamToText } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { z } from 'zod'
const getWeather = toolDefinition({
name: 'getWeather',
description: 'Get weather for a city',
inputSchema: z.object({ city: z.string() }),
outputSchema: z.object({ temp: z.number(), condition: z.string() }),
}).server(async ({ city }) => {
return { temp: 22, condition: `sunny in ${city}` }
})
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: [{ role: 'user', content: 'Weather in Tokyo?' }],
threadId: 'thread-1',
tools: [getWeather],
})
const { text } = await streamToText(stream)
console.log(text)The model decides when to call getWeather. It gets the result and uses it in its answer, all inside one chat() call.
Newline-delimited JSON#
toHttpResponse() returns a Response with newline-delimited JSON instead of SSE. On the client, pair it with fetchHttpStream:
import { chat, chatParamsFromRequest, toHttpResponse } 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 toHttpResponse(stream)
}Read the raw stream#
Iterate the stream with for await to handle each chunk yourself:
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: [{ role: 'user', content: 'Hello!' }],
threadId: 'thread-1',
})
for await (const chunk of stream) {
if (chunk.type === 'TEXT_MESSAGE_CONTENT') {
process.stdout.write(chunk.delta ?? '')
}
}You see every chunk type: text deltas, tool calls, run lifecycle events, and more. For more control, see StreamProcessor.
OpenRouter key#
To use OpenRouter, put its key in .env:
OPENROUTER_API_KEY=sk-or-...Then use the adapter from OpenRouter.