【2026年版】Next.js + AI SDKで作るチャットボットアプリ開発チュートリアル

Tech Trends AI
- 3 minutes read - 569 wordsはじめに
AIチャットボットは、カスタマーサポート、社内ヘルプデスク、学習支援など幅広い用途で需要が高まっています。Next.jsとVercel AI SDKを組み合わせることで、ストリーミング応答に対応した高品質なチャットボットを効率的に開発できます。
本チュートリアルでは、プロジェクト作成からデプロイまでの全工程をステップバイステップで解説します。
完成イメージと技術スタック
技術スタック
| 技術 | 用途 | バージョン |
|---|---|---|
| Next.js | フレームワーク | 15.x |
| Vercel AI SDK | AI統合 | 4.x |
| React | UI | 19.x |
| TypeScript | 型安全性 | 5.x |
| Tailwind CSS | スタイリング | 4.x |
| Anthropic Claude | LLMバックエンド | claude-sonnet-4-5 |
主要機能
- リアルタイムストリーミング応答
- 会話履歴の保持
- Markdown表示対応
- レスポンシブUI
- エラーハンドリング
ステップ1:プロジェクトのセットアップ
Next.jsプロジェクトの作成
npx create-next-app@latest ai-chatbot --typescript --tailwind --app --src-dir
cd ai-chatbot
必要なパッケージのインストール
npm install ai @ai-sdk/anthropic react-markdown
環境変数の設定
# .env.local
ANTHROPIC_API_KEY=your-api-key-here
ステップ2:APIルートの作成
チャットAPIエンドポイント
// src/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-sonnet-4-5-20250929'),
system: `あなたは親切で知識豊富なAIアシスタントです。
日本語で丁寧に回答してください。
技術的な質問には具体的なコード例を含めて回答してください。`,
messages,
maxTokens: 2048,
});
return result.toDataStreamResponse();
}
ポイント解説
streamText: ストリーミング応答を簡単に実装toDataStreamResponse(): Vercel AI SDK互換のストリームレスポンスを生成maxDuration: サーバーレス関数のタイムアウト設定
ステップ3:チャットUIの構築
メインチャットコンポーネント
// src/app/page.tsx
'use client';
import { useChat } from 'ai/react';
import { ChatMessage } from '@/components/ChatMessage';
import { ChatInput } from '@/components/ChatInput';
export default function ChatPage() {
const { messages, input, handleInputChange, handleSubmit, isLoading, error } =
useChat({
api: '/api/chat',
});
return (
<div className="flex flex-col h-screen bg-gray-50">
<header className="bg-white border-b px-6 py-4">
<h1 className="text-xl font-bold text-gray-800">
AI チャットアシスタント
</h1>
</header>
<main className="flex-1 overflow-y-auto px-4 py-6">
<div className="max-w-3xl mx-auto space-y-4">
{messages.length === 0 && (
<div className="text-center text-gray-500 mt-20">
<p className="text-lg">何でも質問してください</p>
<p className="text-sm mt-2">
AIアシスタントが日本語で回答します
</p>
</div>
)}
{messages.map((message) => (
<ChatMessage key={message.id} message={message} />
))}
{error && (
<div className="bg-red-50 text-red-600 p-4 rounded-lg">
エラーが発生しました。もう一度お試しください。
</div>
)}
</div>
</main>
<ChatInput
input={input}
handleInputChange={handleInputChange}
handleSubmit={handleSubmit}
isLoading={isLoading}
/>
</div>
);
}
メッセージ表示コンポーネント
// src/components/ChatMessage.tsx
import ReactMarkdown from 'react-markdown';
import type { Message } from 'ai';
interface ChatMessageProps {
message: Message;
}
export function ChatMessage({ message }: ChatMessageProps) {
const isUser = message.role === 'user';
return (
<div className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}>
<div
className={`max-w-[80%] rounded-2xl px-4 py-3 ${
isUser
? 'bg-blue-600 text-white'
: 'bg-white border border-gray-200 text-gray-800'
}`}
>
{isUser ? (
<p>{message.content}</p>
) : (
<ReactMarkdown
className="prose prose-sm max-w-none"
components={{
code({ className, children, ...props }) {
return (
<code
className={`${className} bg-gray-100 rounded px-1 py-0.5`}
{...props}
>
{children}
</code>
);
},
}}
>
{message.content}
</ReactMarkdown>
)}
</div>
</div>
);
}
入力コンポーネント
// src/components/ChatInput.tsx
import { FormEvent, ChangeEvent } from 'react';
interface ChatInputProps {
input: string;
handleInputChange: (e: ChangeEvent<HTMLTextAreaElement>) => void;
handleSubmit: (e: FormEvent<HTMLFormElement>) => void;
isLoading: boolean;
}
export function ChatInput({
input,
handleInputChange,
handleSubmit,
isLoading,
}: ChatInputProps) {
return (
<footer className="bg-white border-t px-4 py-4">
<form
onSubmit={handleSubmit}
className="max-w-3xl mx-auto flex gap-3"
>
<textarea
value={input}
onChange={handleInputChange}
placeholder="メッセージを入力..."
rows={1}
className="flex-1 resize-none rounded-xl border border-gray-300
px-4 py-3 focus:outline-none focus:ring-2
focus:ring-blue-500 focus:border-transparent"
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmit(e as unknown as FormEvent<HTMLFormElement>);
}
}}
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="bg-blue-600 text-white rounded-xl px-6 py-3
hover:bg-blue-700 disabled:opacity-50
disabled:cursor-not-allowed transition-colors"
>
{isLoading ? '送信中...' : '送信'}
</button>
</form>
</footer>
);
}
ステップ4:機能の拡張
会話のリセット機能
const { messages, setMessages, ...rest } = useChat({ api: '/api/chat' });
const handleReset = () => {
setMessages([]);
};
ストリーミング中の中断
const { stop, isLoading } = useChat({ api: '/api/chat' });
// ストリーミング中に「停止」ボタンを表示
{isLoading && (
<button onClick={stop} className="text-red-500">
生成を停止
</button>
)}
ステップ5:デプロイ
Vercelへのデプロイ
npm install -g vercel
vercel
Vercelダッシュボードで環境変数 ANTHROPIC_API_KEY を設定します。
セルフホスティング
npm run build
npm start
Docker化する場合:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]
まとめ
Next.jsとVercel AI SDKを使うことで、以下の機能を備えたAIチャットボットを短時間で構築できます。
- ストリーミング応答:
streamText+useChatでリアルタイム表示 - 型安全な開発: TypeScriptによる堅牢なコード
- モダンUI: Tailwind CSSによるレスポンシブデザイン
- 柔軟なデプロイ: Vercelまたはセルフホスティング
このベースを拡張して、RAG連携やツール呼び出しなどより高度な機能を追加していくことが可能です。