This guide builds a complete AI chatbot from scratch using Next.js App Router and Claude. By the end, you’ll have a streaming chatbot with conversation history, ready to deploy to Vercel.


What We’re Building

  • Next.js 15 App Router project
  • Streaming AI responses using Vercel AI SDK
  • Claude 3.5 Sonnet as the model
  • Persistent conversation history in the UI
  • Clean chat UI with Tailwind CSS

Setup

npx create-next-app@latest ai-chatbot --typescript --tailwind --app
cd ai-chatbot
npm install ai @ai-sdk/anthropic

Add your API key to .env.local:

ANTHROPIC_API_KEY=sk-ant-...

API Route

Create app/api/chat/route.ts:

import { anthropic } from '@ai-sdk/anthropic';
import { streamText } from 'ai';

export const maxDuration = 30;

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: anthropic('claude-3-5-sonnet-20241022'),
    system: 'You are a helpful assistant.',
    messages,
  });

  return result.toDataStreamResponse();
}

Chat UI Component

Create app/components/Chat.tsx:

'use client';

import { useChat } from 'ai/react';

export default function Chat() {
  const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat();

  return (
    <div className="flex flex-col h-screen max-w-2xl mx-auto p-4">
      {/* Messages */}
      <div className="flex-1 overflow-y-auto space-y-4 mb-4">
        {messages.length === 0 && (
          <div className="text-center text-gray-400 mt-8">
            Start a conversation with Claude
          </div>
        )}
        {messages.map((message) => (
          <div
            key={message.id}
            className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
          >
            <div
              className={`max-w-[80%] rounded-2xl px-4 py-2 ${
                message.role === 'user'
                  ? 'bg-blue-600 text-white'
                  : 'bg-gray-100 text-gray-900'
              }`}
            >
              <p className="whitespace-pre-wrap">{message.content}</p>
            </div>
          </div>
        ))}
        {isLoading && (
          <div className="flex justify-start">
            <div className="bg-gray-100 rounded-2xl px-4 py-2">
              <div className="flex space-x-1">
                <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" />
                <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }} />
                <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.4s' }} />
              </div>
            </div>
          </div>
        )}
      </div>

      {/* Input */}
      <form onSubmit={handleSubmit} className="flex gap-2">
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="Ask anything..."
          className="flex-1 rounded-full border border-gray-300 px-4 py-2 focus:outline-none focus:border-blue-500"
          disabled={isLoading}
        />
        <button
          type="submit"
          disabled={isLoading || !input.trim()}
          className="bg-blue-600 text-white rounded-full px-6 py-2 disabled:opacity-50 hover:bg-blue-700"
        >
          Send
        </button>
      </form>
    </div>
  );
}

Page

Update app/page.tsx:

import Chat from './components/Chat';

export default function Home() {
  return (
    <main>
      <Chat />
    </main>
  );
}

Adding a System Prompt Selector

Extend the chatbot with configurable system prompts:

// app/api/chat/route.ts
const SYSTEM_PROMPTS: Record<string, string> = {
  default: 'You are a helpful assistant.',
  coder: 'You are an expert developer. Provide working code with explanations.',
  writer: 'You are a writing coach. Help improve writing with specific feedback.',
  analyst: 'You are a data analyst. Provide structured analysis and insights.',
};

export async function POST(req: Request) {
  const { messages, persona = 'default' } = await req.json();

  const result = streamText({
    model: anthropic('claude-3-5-sonnet-20241022'),
    system: SYSTEM_PROMPTS[persona] ?? SYSTEM_PROMPTS.default,
    messages,
  });

  return result.toDataStreamResponse();
}

Adding Rate Limiting

npm install @upstash/ratelimit @upstash/redis
// app/api/chat/route.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '1 m'), // 10 requests per minute
});

export async function POST(req: Request) {
  const ip = req.headers.get('x-forwarded-for') ?? '127.0.0.1';
  const { success } = await ratelimit.limit(ip);
  
  if (!success) {
    return new Response('Too many requests', { status: 429 });
  }

  const { messages } = await req.json();
  // ... rest of handler
}

Deployment to Vercel

vercel

Add environment variable in Vercel dashboard:

  • ANTHROPIC_API_KEY — your Anthropic API key

The API route automatically becomes a serverless function with the correct runtime settings.


Extensions to Build Next

  1. Persistent history — Save conversations to database (Supabase/Neon)
  2. User authentication — Add Clerk or NextAuth
  3. File uploads — Allow users to upload PDFs for analysis
  4. Tool calls — Give Claude access to web search or your data
  5. Custom styling — Replace Tailwind utilities with your design system

This foundation handles the core chatbot pattern. Each extension adds to it without changing the base architecture.