How to Build a Simple AI-Powered Chatbot with Next.js and Claude A developer published a tutorial showing how to build a streaming AI chatbot with Next.js 16.4 and Claude Opus 5.5 using the Vercel AI SDK 7, requiring only an App Router route handler and a client component. The writeup notes breaking changes in AI SDK 7 — the rename of system to instructions, the drop of Node 18 and 20 support in favor of Node 22+, and the replacement of result.toUIMessageStreamResponse() with standalone helpers — plus a Next.js 16 Cache Components setting that fails the build on the default useChat call. Originally published at jy-labs.com https://jy-labs.com/blog/tutorial-nextjs-claude-chatbot . Updated 2026-10-07. You build an AI chatbot with Next.js and Claude from two files: an App Router route handler that calls streamText from the Vercel AI SDK, and a client component that renders the stream with useChat. This version uses Next.js 16.4, AI SDK 7, and Claude Opus 5.5. Every file below type-checks and passes next build as of October 7, 2026. The 2024 version of this post used the OpenAI SDK and a hand-written fetch loop. Both are out of date. AI SDK 7 renamed system to instructions, dropped Node 20, and replaced result.toUIMessageStreamResponse with two standalone helpers. Next.js 16 turned on Cache Components, which fails the build on the default useChat call. You want code you copy once and run, with the model, the cost, and the security tradeoffs stated up front. You need Node.js 22 or later. AI SDK 7 sets "engines": { "node": " =22" } in its package.json and dropped support for Node 18 and 20. I verified this build on Node 22.23.1. Create the app with Tailwind and the App Router, then add the AI SDK packages: npx create-next-app@latest claude-chat --ts --tailwind --eslint --app cd claude-chat npm install ai @ai-sdk/anthropic @ai-sdk/react Versions this post was tested against on October 7, 2026: next 16.4.0 react 19.3.0 ai 7.0.131 @ai-sdk/anthropic 4.0.75 @ai-sdk/react 4.0.134 zod 4.6.5 pulled in as a peer dependency, you do not import it here All three AI SDK packages are ESM-only in version 7. If you have an older require based config somewhere, convert it to import first. Create an API key in the Claude Console, then add it to .env.local at the project root: ANTHROPIC API KEY=sk-ant-... The @ai-sdk/anthropic provider reads ANTHROPIC API KEY from the environment by default, so you never pass the key in code. Two rules keep it private: NEXT PUBLIC . Next.js inlines any Add .env.local to .gitignore if create-next-app did not already do it. On Vercel, set the same variable under Project Settings, Environment Variables, and leave it unchecked for the client. Create app/api/chat/route.ts . This is the only file that talks to Anthropic. js import { anthropic } from '@ai-sdk/anthropic'; import { convertToModelMessages, createUIMessageStreamResponse, streamText, toUIMessageStream, type UIMessage, } from 'ai'; // One place to change the model. See the FAQ for the cost of each option. const MODEL = 'claude-opus-5-5'; // Only the last N messages go to the model. Caps input tokens per request. const MAX HISTORY = 20; // Allow streaming responses up to 30 seconds on Vercel. export const maxDuration = 30; export async function POST req: Request { const { messages }: { messages: UIMessage } = await req.json ; const result = streamText { model: anthropic MODEL , instructions: 'You are a concise assistant for a small business website. ' + 'Answer in plain language. If you do not know, say so.', messages: await convertToModelMessages messages.slice -MAX HISTORY , maxOutputTokens: 1024, // Stops the Anthropic request when the browser aborts the fetch. abortSignal: req.signal, providerOptions: { anthropic: { // Chat does not need deep reasoning. 'low' cuts latency and output tokens. effort: 'low', // If Claude's safety classifiers decline a request, Anthropic re-runs it // on a fallback model inside the same call. The provider adds the beta header. fallbacks: 'default', }, }, onError: { error } = { console.error ' chat stream error', error ; }, } ; return createUIMessageStreamResponse { stream: toUIMessageStream { stream: result.stream, // The client sees this string instead of the raw error. onError: = 'The assistant is unavailable right now. Try again in a moment.', } , } ; } What each piece does: anthropic MODEL builds the model reference. claude-opus-5-5 is the current Opus model. The FAQ covers swapping to Sonnet or Haiku. instructions is the system prompt. AI SDK 7 renamed it from system . The old name still works with a deprecation warning. Version 7 also rejects role: "system" entries inside messages by default, so if you persist chat history, keep system text out of it. convertToModelMessages strips UI metadata from the UIMessage the client sends and returns the ModelMessage shape the model expects. It is async in version 6 and later, so await it. messages.slice -MAX HISTORY bounds input tokens. Without it, a long session re-sends the whole transcript on every turn and your cost grows with conversation length. maxOutputTokens: 1024 caps the reply. Raise it if your use case needs long answers. abortSignal: req.signal cancels the Anthropic request when the user clicks Stop. Without it the server keeps generating tokens you pay for and nobody reads. effort: "low" tells Claude to spend fewer thinking tokens. Opus 5.5 defaults to medium . A website chat widget rarely needs more than low , and the difference shows up in both latency and output cost. fallbacks: "default" opts into Anthropic server-side refusal fallbacks. If a safety classifier declines a request, the API re-runs it on a fallback model in the same call. The provider adds the required beta header for you. toUIMessageStream plus createUIMessageStreamResponse replace the result.toUIMessageStreamResponse method from version 6. The old method still works in 7 with a warning and is scheduled for removal in the next major. onError on streamText is for server logs only. maxDuration = 30 lets a Vercel function stream for up to 30 seconds. Other hosts ignore it. Replace app/page.tsx with a client component. useChat owns the message list, the request lifecycle, and the abort controller. js 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useState } from 'react'; export default function Chat { const input, setInput = useState '' ; const { messages, sendMessage, status, stop, error, regenerate } = useChat { // A fixed id keeps the prerender deterministic under Next.js Cache Components. id: 'site-chat', transport: new DefaultChatTransport { api: '/api/chat' } , } ; const busy = status === 'submitted' || status === 'streaming'; return