22title : Basic Chat
33id : basic-chat
44order : 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 ."
66keywords :
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
7867import { useState } from ' react'
79- import { createFileRoute } from ' @tanstack/react-router'
8068import { 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'
8670import { byok } from ' @/lib/byok'
8771
8872function 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
131131function 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'
193200import { 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
209266The same app is on the Examples tab at ` /ai/latest/docs/framework/react/examples/basic-chat ` .
210267
0 commit comments