Vercel AI SDK Chat Template

Workflow

Next.js chat UI template using Vercel AI SDK with streaming and tool calling.

Vercel AI SDK Chat Template

Overview

This template provides a complete Next.js chat UI using the Vercel AI SDK. It includes streaming responses, tool calling, structured outputs, and a production-ready chat interface.

The Vercel AI SDK provides a unified API for calling any LLM, with excellent TypeScript support and seamless Next.js integration.

Prerequisites

  • Node.js 18+
  • An LLM provider API key (OpenAI, Anthropic, etc.)

Project Structure

vercel-ai-chat/
├── app/
│   ├── api/
│   │   └── chat/
│   │       └── route.ts    # Chat API endpoint
│   ├── page.tsx            # Main chat UI page
│   └── layout.tsx          # Root layout
│   └── globals.css         # Global styles
├── components/
│   └── chat-ui.tsx         # Reusable chat component
├── lib/
│   └── tools.ts            # Tool definitions
│   └── utils.ts            # Utility functions
├── .env.local              # Environment variables
├── next.config.js
├── package.json
└── tsconfig.json

Installation

# Create Next.js app
npx create-next-app@latest vercel-ai-chat --typescript --tailwind --app

# Install AI SDK and provider
cd vercel-ai-chat
npm install ai @ai-sdk/openai
# or for Anthropic:
# npm install ai @ai-sdk/anthropic

# Create .env.local
echo "OPENAI_API_KEY=sk-..." > .env.local

Configuration

Environment Variables

.env.local:

OPENAI_API_KEY=sk-your-openai-api-key
# or
ANTHROPIC_API_KEY=sk-ant-your-anthropic-api-key

Next.js Configuration

next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  // AI SDK works out of the box with Next.js App Router
};

module.exports = nextConfig;

Core Implementation

1. Chat API Route

app/api/chat/route.ts:

import { openai } from '@ai-sdk/openai';
import { streamText, convertToCoreMessages } from 'ai';
import { getWeather, searchWeb } from '@/lib/tools';

// Allow streaming responses up to 30 seconds
export const maxDuration = 30;

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

  const result = await streamText({
    model: openai('gpt-4o'),
    messages: convertToCoreMessages(messages),
    tools: {
      getWeather,
      searchWeb,
    },
    system: `You are a helpful AI assistant. Be concise and helpful.
    
    If you need to use tools, explain what you're doing.
    If you don't know something, say so honestly.`,
  });

  return result.toDataStreamResponse();
}

2. Tool Definitions

lib/tools.ts:

import { tool } from 'ai';
import { z } from 'zod';

export const getWeather = tool({
  description: 'Get the current weather for a location',
  parameters: z.object({
    location: z.string().describe('The city name, e.g., "San Francisco"'),
  }),
  execute: async ({ location }) => {
    // Replace with actual weather API call
    // Example: const response = await fetch(`https://api.weather.com/...`);
    return {
      location,
      temperature: 72,
      condition: 'sunny',
      humidity: 45,
    };
  },
});

export const searchWeb = tool({
  description: 'Search the web for current information',
  parameters: z.object({
    query: z.string().describe('The search query'),
  }),
  execute: async ({ query }) => {
    // Replace with actual search API (Brave, Serper, etc.)
    return {
      results: [
        { title: `Search result for: ${query}`, url: 'https://example.com' },
      ],
    };
  },
});

3. Chat UI Component

app/page.tsx:

'use client';

import { useChat } from 'ai/react';
import { useState } from 'react';

export default function ChatPage() {
  const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
    api: '/api/chat',
    onFinish: (message) => {
      console.log('Finished streaming:', message);
    },
    onError: (error) => {
      console.error('Chat error:', error);
    },
  });

  return (
    <div className="flex flex-col h-screen max-w-3xl mx-auto">
      {/* Header */}
      <header className="border-b p-4">
        <h1 className="text-xl font-bold">AI Chat</h1>
      </header>

      {/* Messages */}
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {messages.map((m) => (
          <div
            key={m.id}
            className={`flex ${
              m.role === 'user' ? 'justify-end' : 'justify-start'
            }`}
          >
            <div
              className={`max-w-[80%] rounded-lg p-4 ${
                m.role === 'user'
                  ? 'bg-blue-500 text-white'
                  : 'bg-gray-100 text-gray-900'
              }`}
            >
              <div className="font-semibold text-sm mb-1">
                {m.role === 'user' ? 'You' : 'AI'}
              </div>
              <div className="whitespace-pre-wrap">{m.content}</div>
              
              {/* Show tool calls */}
              {m.toolInvocations?.map((toolCall) => (
                <div key={toolCall.toolCallId} className="mt-2 text-sm text-gray-500">
                  {toolCall.state === 'call' && (
                    <div>Calling {toolCall.toolName}...</div>
                  )}
                  {toolCall.state === 'result' && (
                    <div>
                      <strong>{toolCall.toolName}:</strong>
                      <pre className="mt-1 bg-gray-50 p-2 rounded text-xs overflow-auto">
                        {JSON.stringify(toolCall.result, null, 2)}
                      </pre>
                    </div>
                  )}
                </div>
              ))}
            </div>
          </div>
        ))}
        {isLoading && (
          <div className="flex justify-start">
            <div className="bg-gray-100 rounded-lg p-4">
              <div className="animate-pulse">Thinking...</div>
            </div>
          </div>
        )}
      </div>

      {/* Input */}
      <form onSubmit={handleSubmit} className="border-t p-4">
        <div className="flex gap-2">
          <input
            className="flex-1 border rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
            value={input}
            onChange={handleInputChange}
            placeholder="Type your message..."
            disabled={isLoading}
          />
          <button
            type="submit"
            disabled={isLoading || !input.trim()}
            className="bg-blue-500 text-white rounded-lg p-3 px-6 disabled:opacity-50"
          >
            Send
          </button>
        </div>
      </form>
    </div>
  );
}

Advanced Features

Structured Outputs

// In your API route
import { streamObject } from 'ai';
import { z } from 'zod';

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

  const result = await streamObject({
    model: openai('gpt-4o'),
    messages,
    schema: z.object({
      summary: z.string(),
      sentiment: z.enum(['positive', 'neutral', 'negative']),
      keyPoints: z.array(z.string()),
    }),
  });

  return result.toTextStreamResponse();
}

Multi-Model Support

import { createOpenAI } from '@ai-sdk/openai';
import { createAnthropic } from '@ai-sdk/anthropic';
import { createFallback } from '@ai-sdk/fallback';

const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

const model = createFallback({
  models: [openai('gpt-4o'), anthropic('claude-3-5-sonnet-20241022')],
  fallbackStrategy: 'on-error',
});

RAG Integration

// lib/vector-store.ts
import { embed } from 'ai';
import { openai } from '@ai-sdk/openai';

export async function searchKnowledgeBase(query: string) {
  const { embedding } = await embed({
    model: openai('text-embedding-3-small'),
    value: query,
  });

  // Query your vector database
  // const results = await vectorDb.query({ embedding, k: 5 });
  
  return results;
}

// In your chat route
import { searchKnowledgeBase } from '@/lib/vector-store';

const relevantDocs = await searchKnowledgeBase(userQuery);

const result = await streamText({
  model: openai('gpt-4o'),
  messages,
  prompt: `Context: ${relevantDocs}\n\nQuestion: ${userQuery}`,
});

Styling

app/globals.css:

@tailwind base;
@tailwind components;
@tailwind utilities;

/* Custom scrollbar */
::-webkit-scrollbar {
  width: 8px;
}

::-webkit-scrollbar-track {
  background: #f1f1f1;
}

::-webkit-scrollbar-thumb {
  background: #888;
  border-radius: 4px;
}

::-webkit-scrollbar-thumb:hover {
  background: #555;
}

Running the Project

npm run dev

Open http://localhost:3000 to see the chat UI.

Deployment

# Deploy to Vercel
vercel

# Or build for production
npm run build
npm run start

Resources

View on GitHub

Related Templates