{"slug": "streaming-an-llm-response-in-next-js-15-without-the-ui-feeling-broken", "title": "Streaming an LLM response in Next.js 15 without the UI feeling broken", "summary": "A developer published a Next.js 15 streaming pattern for LLM chat apps using the Vercel AI SDK, showing that `streamText` with `toDataStreamResponse()` on the server and the `useChat` hook on the client handles token streaming, optimistic user messages and loading state with minimal code. The writeup flags two common pitfalls: persisting the assistant message in the browser can truncate output if the client disconnects, so it should be saved server-side in `onFinish`, and usage credits must be checked and spent before the stream starts rather than after, otherwise a user who disconnects mid-stream gets a free generation. The developer also bundled the streaming layer with auth, a credit system and Stripe billing into a paid Next.js 15 starter.", "body_md": "Streaming is table stakes for an AI app now. People expect the answer to appear word by word, not after a 20-second spinner. Here's the setup I use in Next.js 15 with the Vercel AI SDK, plus the two things that quietly trip people up.\n\n``` js\n// app/api/chat/route.ts\nimport { openai } from '@ai-sdk/openai'\nimport { streamText, convertToCoreMessages } from 'ai'\n\nexport async function POST(req: Request) {\n  const { messages } = await req.json()\n\n  const result = streamText({\n    model: openai('gpt-4o-mini'),\n    messages: convertToCoreMessages(messages),\n    onFinish: async ({ text }) => {\n      await saveMessage({ role: 'assistant', content: text })\n    },\n  })\n\n  return result.toDataStreamResponse()\n}\n```\n\nThe provider is one line. Swapping GPT for Claude is `anthropic('claude-...')` and nothing else in the handler changes.\n\n``` js\n'use client'\nimport { useChat } from '@ai-sdk/react'\n\nexport function Chat() {\n  const { messages, input, handleInputChange, handleSubmit, status } = useChat()\n  // render messages, wire the form to handleSubmit\n}\n```\n\n`useChat` handles the streaming, the optimistic user message, and the loading state for you. You render `messages` and you're basically done.\n\nDon't persist the assistant message from the browser. The stream can be cut off (the user closes the tab) and you end up with half a message, or none. Save it on the server in `onFinish` — that fires once, with the complete text, whether or not the client is still listening.\n\nIf you meter usage, check and spend the credit before the stream starts, not in `onFinish`. Charge after and someone who disconnects mid-stream got a free generation. Spend first (atomically, so two requests can't both pass the check — I wrote about the race-safe version [here](https://dev.to/myra_haroon_97a51be23602f/how-i-built-a-credit-system-two-requests-cant-overspend-nextjs-postgres-2knc)), and refund on a rare hard failure if you want to be generous.\n\nModel output is markdown, so render it with react-markdown plus a syntax highlighter (rehype-highlight). Add a copy button on code blocks and a stop button wired to useChat's abort. Small touches, but they're the gap between \"demo\" and \"product.\"\n\nI bundled this streaming layer plus auth, the credit system, and Stripe billing into a Next.js 15 starter so I stop rebuilding it: live demo at [https://ai-saas-starter-ashen.vercel.app](https://ai-saas-starter-ashen.vercel.app), code at [https://venturionai.gumroad.com/l/ai-saas-starter](https://venturionai.gumroad.com/l/ai-saas-starter) (40% off the first 10 with LAUNCH40). The patterns above work on their own though, starter or not.", "url": "https://wpnews.pro/news/streaming-an-llm-response-in-next-js-15-without-the-ui-feeling-broken", "canonical_source": "https://dev.to/myra_haroon_97a51be23602f/streaming-an-llm-response-in-nextjs-15-without-the-ui-feeling-broken-4ffd", "published_at": "2026-10-05 17:41:46+00:00", "updated_at": "2026-10-05 17:48:08.610692+00:00", "lang": "en", "topics": ["large-language-models", "ai-products", "ai-tools", "developer-tools"], "entities": ["Next.js 15", "Vercel AI SDK", "OpenAI", "GPT-4o mini", "Anthropic", "Claude", "Stripe", "react-markdown"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/streaming-an-llm-response-in-next-js-15-without-the-ui-feeling-broken", "markdown": "https://wpnews.pro/news/streaming-an-llm-response-in-next-js-15-without-the-ui-feeling-broken.md", "text": "https://wpnews.pro/news/streaming-an-llm-response-in-next-js-15-without-the-ui-feeling-broken.txt", "jsonld": "https://wpnews.pro/news/streaming-an-llm-response-in-next-js-15-without-the-ui-feeling-broken.jsonld"}}