# 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](./quick-start-react-native).
- **No UI, only a backend:** go to [Quick Start: Server Only](./quick-start-server).

## 1. Install

```sh
npm i @tanstack/ai @tanstack/ai-svelte @tanstack/ai-openai
```

Pick 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](../adapters/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`:

```bash
OPENAI_API_KEY=your-openai-api-key
```

Create a route at `/api/chat`. In Next.js, that is `app/api/chat/route.ts`. Call `chat()`, then return the stream with `toServerSentEventsResponse`:

```typescript
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 `createChat` from `@tanstack/ai-svelte`. The return object uses reactive getters. Read `chat.messages` and `chat.isLoading` with no extra wrapper.

```svelte
<script lang="ts">
import { createChat, fetchServerSentEvents } from '@tanstack/ai-svelte'

let input = $state('')

const chat = createChat({
  connection: fetchServerSentEvents('/api/chat'),
  threadId: 'support-chat',
})

function handleSubmit(event: SubmitEvent) {
  event.preventDefault()
  if (input.trim() && !chat.isLoading) {
    chat.sendMessage(input)
    input = ''
  }
}
</script>

<div>
  {#each chat.messages as message (message.id)}
    <div>
      {#each message.parts as part}
        {#if part.type === 'text'}
          <p>{part.content}</p>
        {/if}
      {/each}
    </div>
  {/each}

  <form onsubmit={handleSubmit}>
    <input bind:value={input} disabled={chat.isLoading} />
    {#if chat.isLoading}
      <button type="button" onclick={() => chat.stop()}>Stop</button>
    {:else}
      <button type="submit" disabled={!input.trim()}>Send</button>
    {/if}
  </form>
</div>
```

If the component can unmount while a response streams, call `chat.stop()` in `onDestroy`.

See the [Svelte API](../api/ai-svelte).

No framework? Use `ChatClient` from `@tanstack/ai-client`. See the [client API](../api/ai-client).

## 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

1. `sendMessage` posts the messages to `/api/chat`.
2. `chatParamsFromRequest` reads the messages and the run ids from the request.
3. `chat()` calls the model through the adapter and returns a stream of events.
4. `toServerSentEventsResponse` sends those events back as Server-Sent Events.
5. `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](../tools/tools): let the model call your functions.
- [Streaming](../chat/streaming): cancel, callbacks, and transports.
- [Adapters](../adapters/openai): switch to another provider.

Or build a full app in a tutorial, such as [Basic Chat](../tutorials/basic-chat) or [Generate Image](../tutorials/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](./quick-start-server).

### 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](../advanced/byok).
