Tutorial Vercel AI SDK: Buat Chatbot AI dengan Next.js
Senin, 29 Des 2025
Vercel AI SDK adalah library yang bikin integrasi AI ke aplikasi web jadi sangat mudah. Dengan SDK ini, kamu bisa bikin chatbot dengan streaming responses, support multiple AI providers, dan fitur advanced seperti tool calling.
Di tutorial ini, saya bakal guide kamu dari setup sampai production-ready chatbot.
Apa itu Vercel AI SDK?
Vercel AI SDK adalah library open-source untuk membangun aplikasi AI. Kelebihannya:
- ✅ Streaming responses - text muncul kata per kata, UX lebih baik
- ✅ Multiple AI providers - OpenAI, Anthropic, Google, dan lainnya
- ✅ React hooks -
useChat,useCompletionyang ready pakai - ✅ Edge-ready - bisa deploy di edge runtime
- ✅ TypeScript first - full type safety
- ✅ Structured outputs - generate JSON dengan schema validation
Setup Project
1. Buat Next.js Project
npx create-next-app@latest ai-chatbot --typescript --tailwind --app
cd ai-chatbot
2. Install Dependencies
npm install ai @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google
SDK-nya modular, jadi kamu cuma perlu install provider yang dipakai.
3. Setup Environment Variables
Buat file .env.local:
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
GOOGLE_GENERATIVE_AI_API_KEY=...
Chatbot Pertama dengan useChat
API Route
Buat app/api/chat/route.ts:
import { openai } from "@ai-sdk/openai";
import { streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai("gpt-4o"),
messages,
system: "Kamu adalah assistant yang helpful. Jawab dalam Bahasa Indonesia.",
});
return result.toDataStreamResponse();
}
Chat Component
Buat app/page.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">
<div className="flex-1 overflow-y-auto space-y-4 mb-4">
{messages.map((message) => (
<div
key={message.id}
className={`p-4 rounded-lg ${
message.role === "user"
? "bg-blue-500 text-white ml-auto max-w-[80%]"
: "bg-gray-100 mr-auto max-w-[80%]"
}`}
>
{message.content}
</div>
))}
{isLoading && (
<div className="bg-gray-100 p-4 rounded-lg mr-auto">
<div className="animate-pulse">Thinking...</div>
</div>
)}
</div>
<form onSubmit={handleSubmit} className="flex gap-2">
<input
value={input}
onChange={handleInputChange}
placeholder="Ketik pesan..."
className="flex-1 p-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="submit"
disabled={isLoading}
className="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50"
>
Kirim
</button>
</form>
</div>
);
}
Jalankan npm run dev dan chatbot sudah jalan!
Streaming Responses Deep Dive
Kenapa Streaming Penting?
Tanpa streaming, user harus tunggu seluruh response selesai generate. Dengan streaming, text muncul secara real-time - UX jauh lebih baik.
Cara Kerja Streaming
import { streamText } from "ai";
const result = streamText({
model: openai("gpt-4o"),
messages,
});
// Option 1: Data stream (recommended untuk useChat)
return result.toDataStreamResponse();
// Option 2: Text stream (untuk use case lain)
return result.toTextStreamResponse();
Handle Streaming di Client
useChat otomatis handle streaming. Tapi kalau mau manual:
const response = await fetch("/api/chat", {
method: "POST",
body: JSON.stringify({ messages }),
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader!.read();
if (done) break;
const chunk = decoder.decode(value);
console.log(chunk); // Text chunk
}
Multiple AI Providers
Salah satu kekuatan Vercel AI SDK adalah unified API untuk berbagai providers.
OpenAI
import { openai } from "@ai-sdk/openai";
const result = streamText({
model: openai("gpt-4o"), // atau "gpt-4o-mini", "gpt-3.5-turbo"
messages,
});
Anthropic (Claude)
import { anthropic } from "@ai-sdk/anthropic";
const result = streamText({
model: anthropic("claude-sonnet-4-20250514"), // atau "claude-3-5-haiku-20241022"
messages,
});
Google (Gemini)
import { google } from "@ai-sdk/google";
const result = streamText({
model: google("gemini-2.0-flash"), // atau "gemini-1.5-pro"
messages,
});
Dynamic Provider Selection
import { openai } from "@ai-sdk/openai";
import { anthropic } from "@ai-sdk/anthropic";
import { google } from "@ai-sdk/google";
function getModel(provider: string) {
switch (provider) {
case "openai":
return openai("gpt-4o");
case "anthropic":
return anthropic("claude-sonnet-4-20250514");
case "google":
return google("gemini-2.0-flash");
default:
return openai("gpt-4o-mini");
}
}
export async function POST(req: Request) {
const { messages, provider } = await req.json();
const result = streamText({
model: getModel(provider),
messages,
});
return result.toDataStreamResponse();
}
useChat Hook Options
useChat punya banyak options yang powerful:
const {
messages, // Array of messages
input, // Current input value
handleInputChange, // Input onChange handler
handleSubmit, // Form submit handler
isLoading, // Loading state
error, // Error state
reload, // Reload last AI response
stop, // Stop streaming
append, // Append message programmatically
setMessages, // Set messages manually
} = useChat({
api: "/api/chat", // Custom API endpoint
id: "unique-chat-id", // Untuk multiple chats
initialMessages: [], // Pre-populate messages
body: {
// Extra data ke API
userId: "123",
},
headers: {
// Custom headers
Authorization: "Bearer token",
},
onResponse: (response) => {
// Callback saat response diterima
console.log("Response received");
},
onFinish: (message) => {
// Callback saat streaming selesai
console.log("Finished:", message);
},
onError: (error) => {
// Error handling
console.error("Error:", error);
},
});
Contoh: Chat dengan Stop Button
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading, stop } =
useChat();
return (
<div>
{/* Messages */}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
{isLoading ? (
<button type="button" onClick={stop}>
Stop
</button>
) : (
<button type="submit">Kirim</button>
)}
</form>
</div>
);
}
Structured Outputs
Generate JSON dengan schema validation menggunakan Zod:
import { openai } from "@ai-sdk/openai";
import { generateObject } from "ai";
import { z } from "zod";
const result = await generateObject({
model: openai("gpt-4o"),
schema: z.object({
recipe: z.object({
name: z.string(),
ingredients: z.array(
z.object({
name: z.string(),
amount: z.string(),
})
),
steps: z.array(z.string()),
cookingTime: z.number().describe("Cooking time in minutes"),
}),
}),
prompt: "Generate resep nasi goreng",
});
console.log(result.object);
// { recipe: { name: "Nasi Goreng", ingredients: [...], steps: [...], cookingTime: 15 } }
Streaming Structured Output
import { streamObject } from "ai";
const result = streamObject({
model: openai("gpt-4o"),
schema: recipeSchema,
prompt: "Generate resep nasi goreng",
});
for await (const partialObject of result.partialObjectStream) {
console.log(partialObject);
// Object dibangun secara incremental
}
Tool Calling (Function Calling)
Tool calling memungkinkan AI memanggil functions yang kamu define:
import { openai } from "@ai-sdk/openai";
import { streamText, tool } from "ai";
import { z } from "zod";
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai("gpt-4o"),
messages,
tools: {
getWeather: tool({
description: "Get current weather for a location",
parameters: z.object({
location: z.string().describe("City name"),
}),
execute: async ({ location }) => {
// Call weather API
const weather = await fetchWeather(location);
return weather;
},
}),
searchProducts: tool({
description: "Search products in database",
parameters: z.object({
query: z.string(),
category: z.string().optional(),
maxPrice: z.number().optional(),
}),
execute: async ({ query, category, maxPrice }) => {
const products = await db.products.search({
query,
category,
maxPrice,
});
return products;
},
}),
},
maxSteps: 5, // Allow multiple tool calls
});
return result.toDataStreamResponse();
}
Display Tool Results di UI
export default function Chat() {
const { messages } = useChat();
return (
<div>
{messages.map((message) => (
<div key={message.id}>
{message.role === "user" ? (
<div>{message.content}</div>
) : (
<div>
{message.content}
{/* Display tool invocations */}
{message.toolInvocations?.map((tool) => (
<div key={tool.toolCallId} className="bg-gray-50 p-2 rounded">
<div>Tool: {tool.toolName}</div>
{tool.state === "result" && (
<pre>{JSON.stringify(tool.result, null, 2)}</pre>
)}
</div>
))}
</div>
)}
</div>
))}
</div>
);
}
Conversation History & Persistence
Save ke Database
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export async function POST(req: Request) {
const { messages, conversationId } = await req.json();
const result = streamText({
model: openai("gpt-4o"),
messages,
onFinish: async ({ text }) => {
// Save messages ke database
await prisma.message.createMany({
data: [
{
conversationId,
role: "user",
content: messages[messages.length - 1].content,
},
{
conversationId,
role: "assistant",
content: text,
},
],
});
},
});
return result.toDataStreamResponse();
}
Load History
// API: GET /api/chat/[id]
export async function GET(
req: Request,
{ params }: { params: { id: string } }
) {
const messages = await prisma.message.findMany({
where: { conversationId: params.id },
orderBy: { createdAt: "asc" },
});
return Response.json(messages);
}
// Client
const { messages, setMessages } = useChat({
id: conversationId,
});
useEffect(() => {
fetch(`/api/chat/${conversationId}`)
.then((res) => res.json())
.then(setMessages);
}, [conversationId]);
Rate Limiting
Protect API dari abuse dengan rate limiting:
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) {
// Get user identifier
const ip = req.headers.get("x-forwarded-for") ?? "anonymous";
const { success, limit, reset, remaining } = await ratelimit.limit(ip);
if (!success) {
return new Response("Rate limit exceeded", {
status: 429,
headers: {
"X-RateLimit-Limit": limit.toString(),
"X-RateLimit-Remaining": remaining.toString(),
"X-RateLimit-Reset": reset.toString(),
},
});
}
// Process chat...
}
Rate Limit per User
const userId = await getUserId(req); // From auth
const { success } = await ratelimit.limit(`user:${userId}`);
Production Deployment Tips
1. Error Handling
export async function POST(req: Request) {
try {
const { messages } = await req.json();
const result = streamText({
model: openai("gpt-4o"),
messages,
});
return result.toDataStreamResponse();
} catch (error) {
console.error("Chat error:", error);
if (error instanceof Error) {
if (error.message.includes("rate limit")) {
return new Response("Rate limit exceeded", { status: 429 });
}
if (error.message.includes("invalid api key")) {
return new Response("Configuration error", { status: 500 });
}
}
return new Response("Internal server error", { status: 500 });
}
}
2. Timeout Handling
export const maxDuration = 60; // Increase timeout untuk responses panjang
export async function POST(req: Request) {
const result = streamText({
model: openai("gpt-4o"),
messages,
abortSignal: AbortSignal.timeout(55000), // Abort before edge timeout
});
return result.toDataStreamResponse();
}
3. Input Validation
import { z } from "zod";
const requestSchema = z.object({
messages: z.array(
z.object({
role: z.enum(["user", "assistant", "system"]),
content: z.string().max(10000),
})
),
});
export async function POST(req: Request) {
const body = await req.json();
const { messages } = requestSchema.parse(body);
// Process...
}
4. Content Moderation
import { openai } from "@ai-sdk/openai";
async function moderateContent(content: string) {
const response = await openai.moderations.create({
input: content,
});
return response.results[0].flagged;
}
export async function POST(req: Request) {
const { messages } = await req.json();
const lastMessage = messages[messages.length - 1];
if (await moderateContent(lastMessage.content)) {
return new Response("Content violates usage policy", { status: 400 });
}
// Process...
}
5. Logging & Monitoring
export async function POST(req: Request) {
const startTime = Date.now();
const requestId = crypto.randomUUID();
try {
const result = streamText({
model: openai("gpt-4o"),
messages,
onFinish: ({ usage }) => {
console.log({
requestId,
duration: Date.now() - startTime,
promptTokens: usage.promptTokens,
completionTokens: usage.completionTokens,
});
},
});
return result.toDataStreamResponse();
} catch (error) {
console.error({ requestId, error });
throw error;
}
}
Pricing Considerations
Cost per Token
| Provider | Model | Input (per 1M) | Output (per 1M) |
|---|---|---|---|
| OpenAI | GPT-4o | $2.50 | $10.00 |
| OpenAI | GPT-4o-mini | $0.15 | $0.60 |
| Anthropic | Claude 3.5 Sonnet | $3.00 | $15.00 |
| Anthropic | Claude 3.5 Haiku | $0.80 | $4.00 |
| Gemini 1.5 Pro | $1.25 | $5.00 | |
| Gemini 1.5 Flash | $0.075 | $0.30 |
Tips Hemat Cost
- Pakai model yang tepat - GPT-4o-mini cukup untuk banyak use case
- Limit context - Jangan kirim semua history, cukup N messages terakhir
- Cache responses - Untuk pertanyaan yang sama
- Set max tokens - Limit panjang response
const result = streamText({
model: openai("gpt-4o-mini"), // Lebih murah
messages: messages.slice(-10), // Hanya 10 messages terakhir
maxTokens: 500, // Limit response length
});
Complete Example: Production Chatbot
// app/api/chat/route.ts
import { openai } from "@ai-sdk/openai";
import { streamText, tool } from "ai";
import { z } from "zod";
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(20, "1 m"),
});
const requestSchema = z.object({
messages: z
.array(
z.object({
role: z.enum(["user", "assistant", "system"]),
content: z.string().max(10000),
})
)
.max(50),
});
export const maxDuration = 60;
export async function POST(req: Request) {
try {
// Rate limiting
const ip = req.headers.get("x-forwarded-for") ?? "anonymous";
const { success } = await ratelimit.limit(ip);
if (!success) {
return new Response("Rate limit exceeded", { status: 429 });
}
// Validate input
const body = await req.json();
const { messages } = requestSchema.parse(body);
// Stream response
const result = streamText({
model: openai("gpt-4o-mini"),
system: `Kamu adalah assistant yang helpful.
Jawab dalam Bahasa Indonesia yang natural.
Jika tidak tahu, bilang tidak tahu.`,
messages: messages.slice(-20), // Limit context
maxTokens: 1000,
tools: {
getCurrentTime: tool({
description: "Get current time",
parameters: z.object({}),
execute: async () => new Date().toISOString(),
}),
},
});
return result.toDataStreamResponse();
} catch (error) {
if (error instanceof z.ZodError) {
return new Response("Invalid request", { status: 400 });
}
console.error("Chat error:", error);
return new Response("Internal server error", { status: 500 });
}
}
Kesimpulan
Vercel AI SDK bikin development chatbot jadi jauh lebih mudah:
- Setup cepat - Beberapa menit sudah bisa streaming
- Unified API - Ganti provider tanpa ubah banyak code
- Production-ready - Built-in features untuk scale
- Type-safe - Full TypeScript support
Langkah selanjutnya:
- Explore AI SDK Documentation
- Coba template di Vercel AI Templates
- Experiment dengan tool calling untuk use case spesifik
Selamat ngoding! 🚀
Ada pertanyaan tentang implementasi Vercel AI SDK? Reach out di Twitter @nayakayp!