Skip to content

Commit 42f538b

Browse files
committed
docs: rewrite Basic Chat tutorial as a Start walkthrough
Create a Start app, install packages, then BYOK, useChat, and a two-step server route.
1 parent 914e459 commit 42f538b

1 file changed

Lines changed: 119 additions & 62 deletions

File tree

docs/tutorials/basic-chat.md

Lines changed: 119 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: Basic Chat
33
id: basic-chat
44
order: 1
5-
description: "Build a streaming React chat on TanStack Start. A server route streams tokens, useChat renders them, and BYOK keeps the OpenRouter key in the tab."
5+
description: "Create a TanStack Start app, then add a streaming React chat. BYOK holds the OpenRouter key in the tab. useChat talks to a server route that streams tokens."
66
keywords:
77
- tanstack ai
88
- tutorial
@@ -13,76 +13,60 @@ keywords:
1313
- tanstack start
1414
---
1515

16-
Build a streaming chat. Keep the OpenRouter key off the server.
16+
Create a TanStack Start app. Then add a streaming chat.
1717

18-
A Start route streams tokens. The client uses `useChat`. BYOK keeps the key in this tab.
18+
This tutorial is React + Start. For other frameworks, open [Quick Start](../getting-started/quick-start).
1919

20-
Copy the files below. Or open the live sandbox. Paste a key there.
20+
You can skip the scaffold and paste a key in the sandbox at the end of this page.
2121

22-
This tutorial is React + Start. For other frameworks, open [Quick Start](../getting-started/quick-start).
22+
## 1. Create a Start app
2323

24-
> [!TIP]
25-
> The app uses `@tanstack/ai`, `@tanstack/ai-react`, `@tanstack/ai-openrouter`, and `@tanstack/ai-client`. OpenRouter keys come from [openrouter.ai](https://openrouter.ai).
24+
```bash
25+
npx @tanstack/cli@latest create
26+
```
2627

27-
## 1. Stream from a Start route
28+
Pick React. For more options, see [Start getting started](https://tanstack.com/start/latest/docs/framework/react/quick-start).
2829

29-
Put this in `src/routes/api.chat.ts`. It reads the chat body and the OpenRouter key. Then it returns an SSE stream.
30+
Then install the TanStack AI packages:
3031

31-
```typescript
32-
import { createFileRoute } from '@tanstack/react-router'
33-
import {
34-
chat,
35-
chatParamsFromRequest,
36-
toServerSentEventsResponse,
37-
} from '@tanstack/ai'
38-
import { createOpenRouterText } from '@tanstack/ai-openrouter'
39-
import { openrouterByok } from '@tanstack/ai-openrouter/byok'
40-
import { byokMissing, getByokKey } from '@tanstack/ai/byok/server'
32+
<!-- ::start:tabs variant="package-manager" mode="install" -->
4133

42-
export async function POST({ request }: { request: Request }) {
43-
const params = await chatParamsFromRequest(request)
44-
const apiKey = getByokKey(request, openrouterByok)
45-
if (!apiKey) return byokMissing(openrouterByok)
34+
react: @tanstack/ai @tanstack/ai-react @tanstack/ai-client @tanstack/ai-openrouter
4635

47-
const stream = chat({
48-
adapter: createOpenRouterText('openai/gpt-5.5', apiKey),
49-
messages: params.messages,
50-
threadId: params.threadId,
51-
runId: params.runId,
52-
})
53-
return toServerSentEventsResponse(stream)
54-
}
36+
<!-- ::end:tabs -->
5537

56-
export const Route = createFileRoute('/api/chat')({
57-
server: {
58-
handlers: {
59-
POST,
60-
},
61-
},
62-
})
63-
```
38+
Get an OpenRouter key from [openrouter.ai](https://openrouter.ai).
39+
40+
## Client and server
41+
42+
A chat has two sides.
43+
44+
The **client** runs in the browser. It holds the key, draws messages, and POSTs to your route.
6445

65-
If the key is missing, `byokMissing` returns HTTP 401.
46+
The **server** route reads that key, calls OpenRouter, and streams tokens back.
6647

67-
## 2. Render with `useChat`
48+
The next steps build the client. Then they add the route.
6849

69-
Call `useChat` on the home route with:
50+
## 2. Set up BYOK on the client
51+
52+
Create `src/lib/byok.ts`. `memoryStorage()` keeps the key in this tab.
53+
54+
```typescript
55+
import { defineByok, memoryStorage } from '@tanstack/ai-client/byok'
56+
import { openrouterByok } from '@tanstack/ai-openrouter/byok'
7057

71-
- `connection`: `fetchServerSentEvents('/api/chat')`
72-
- `byok`: the BYOK store
73-
- `forwardedProps`: provider `openrouter` and model `openai/gpt-5.5`
58+
export const byok = defineByok({
59+
storage: memoryStorage(),
60+
providers: [openrouterByok],
61+
})
62+
```
7463

75-
Put this in `src/routes/index.tsx`. The `byok` import is the next file.
64+
Add a paste field. `byok.update` saves the key. `useByok` reads the status.
7665

7766
```tsx
7867
import { useState } from 'react'
79-
import { createFileRoute } from '@tanstack/react-router'
8068
import { openrouterByok } from '@tanstack/ai-openrouter/byok'
81-
import {
82-
fetchServerSentEvents,
83-
useByok,
84-
useChat,
85-
} from '@tanstack/ai-react'
69+
import { useByok } from '@tanstack/ai-react'
8670
import { byok } from '@/lib/byok'
8771

8872
function OpenRouterKeyForm() {
@@ -127,6 +111,22 @@ function OpenRouterKeyForm() {
127111
</form>
128112
)
129113
}
114+
```
115+
116+
If you want passkeys, open [Bring Your Own Key](../advanced/byok).
117+
118+
## 3. Hook up `useChat`
119+
120+
Put this in `src/routes/index.tsx`. Pass `byok` and `forwardedProps` on the hook. `useChat` sends the key in an `x-byok-*` header.
121+
122+
```tsx
123+
import { useState } from 'react'
124+
import { createFileRoute } from '@tanstack/react-router'
125+
import {
126+
fetchServerSentEvents,
127+
useChat,
128+
} from '@tanstack/ai-react'
129+
import { byok } from '@/lib/byok'
130130

131131
function ChatPage() {
132132
const [input, setInput] = useState('')
@@ -184,27 +184,84 @@ export const Route = createFileRoute('/')({
184184

185185
`messages` updates as tokens arrive. Click Stop to cancel.
186186

187-
## 3. Keep the key in the tab
187+
A send with no key does not POST. The form shows "Paste an OpenRouter key, then send again."
188188

189-
Create `src/lib/byok.ts`. `memoryStorage()` keeps the key in this tab.
189+
## 4. Add the server route
190+
191+
Create `src/routes/api.chat.ts`. Do this in two steps.
192+
193+
### Read the key
194+
195+
`getByokKey` reads the `x-byok-openrouter` header, then `OPENROUTER_API_KEY` in the environment. If both are empty, `byokMissing` returns HTTP 401.
190196

191197
```typescript
192-
import { defineByok, memoryStorage } from '@tanstack/ai-client/byok'
198+
import { createFileRoute } from '@tanstack/react-router'
199+
import { chatParamsFromRequest } from '@tanstack/ai'
193200
import { openrouterByok } from '@tanstack/ai-openrouter/byok'
201+
import { byokMissing, getByokKey } from '@tanstack/ai/byok/server'
194202

195-
export const byok = defineByok({
196-
storage: memoryStorage(),
197-
providers: [openrouterByok],
203+
export async function POST({ request }: { request: Request }) {
204+
const params = await chatParamsFromRequest(request)
205+
const apiKey = getByokKey(request, openrouterByok)
206+
if (!apiKey) return byokMissing(openrouterByok)
207+
208+
return new Response('ok')
209+
}
210+
211+
export const Route = createFileRoute('/api/chat')({
212+
server: {
213+
handlers: {
214+
POST,
215+
},
216+
},
198217
})
199218
```
200219

201-
The paste field saves with `byok.update(openrouterByok.id, next)`. `useChat` sends that key on an `x-byok-*` header.
220+
This is a stub. The next step replaces the `ok` body.
202221

203-
If you want passkeys, open [Bring Your Own Key](../advanced/byok).
222+
Import `openrouterByok` from `@tanstack/ai-openrouter/byok`, not from the adapter main entry.
223+
224+
### Call `chat` and return the stream
225+
226+
Replace the `ok` response. Pass the key into `createOpenRouterText`. Wrap `chat()` with `toServerSentEventsResponse`.
227+
228+
```typescript
229+
import { createFileRoute } from '@tanstack/react-router'
230+
import {
231+
chat,
232+
chatParamsFromRequest,
233+
toServerSentEventsResponse,
234+
} from '@tanstack/ai'
235+
import { createOpenRouterText } from '@tanstack/ai-openrouter'
236+
import { openrouterByok } from '@tanstack/ai-openrouter/byok'
237+
import { byokMissing, getByokKey } from '@tanstack/ai/byok/server'
238+
239+
export async function POST({ request }: { request: Request }) {
240+
const params = await chatParamsFromRequest(request)
241+
const apiKey = getByokKey(request, openrouterByok)
242+
if (!apiKey) return byokMissing(openrouterByok)
243+
244+
const stream = chat({
245+
adapter: createOpenRouterText('openai/gpt-5.5', apiKey),
246+
messages: params.messages,
247+
threadId: params.threadId,
248+
runId: params.runId,
249+
})
250+
return toServerSentEventsResponse(stream)
251+
}
252+
253+
export const Route = createFileRoute('/api/chat')({
254+
server: {
255+
handlers: {
256+
POST,
257+
},
258+
},
259+
})
260+
```
204261

205-
## 4. Try it live
262+
## 5. Try it
206263

207-
Paste an OpenRouter key in the sandbox. Send a message. Tokens stream into the UI.
264+
Run the app. Paste an OpenRouter key. Send a message. Tokens stream into the UI.
208265

209266
The same app is on the Examples tab at `/ai/latest/docs/framework/react/examples/basic-chat`.
210267

0 commit comments

Comments
 (0)