Tasuke HubLearn · Solve · Grow
#Next.js

Next.jsとTypeScriptでAI統合Webアプリを構築する完全ガイド【2025年最新】

Vercel AI SDKとReact Server Componentsを活用し、OpenAI APIと統合したストリーミング対応チャットボットを実装。セキュリティとパフォーマンスを重視した実用的なコード例付き。

時計のアイコン12 August, 2025

2025年、AI統合Webアプリケーション開発は新たな段階に突入しました。Next.js 15とReact Server Componentsの普及により、従来よりも効率的でパフォーマンスの高いAI統合が可能になっています。

本記事では、Vercel AI SDKを活用してOpenAI APIと統合したストリーミング対応チャットボットを構築します。実際のプロダクション環境で使用できる完全なコードとベストプラクティスを提供します。

TH

Tasuke Hub管理人

東証プライム市場上場企業エンジニア

情報系修士卒業後、大手IT企業にてフルスタックエンジニアとして活躍。 Webアプリケーション開発からクラウドインフラ構築まで幅広い技術に精通し、 複数のプロジェクトでリードエンジニアを担当。 技術ブログやオープンソースへの貢献を通じて、日本のIT技術コミュニティに積極的に関わっている。

🎓情報系修士🏢東証プライム上場企業💻フルスタックエンジニア📝技術ブログ執筆者

技術スタック概要

今回使用する主要技術:

  • Next.js 15 - App Routerとサーバーコンポーネント
  • TypeScript - 型安全性の確保
  • Vercel AI SDK - AI統合の抽象化レイヤー
  • OpenAI API - GPT-4を使用した会話AI
  • React Server Components - パフォーマンス最適化
ベストマッチ

最短で課題解決する一冊

この記事の内容と高い親和性が確認できたベストマッチです。早めにチェックしておきましょう。

プロジェクト環境構築

1. 新規プロジェクトの作成

npx create-next-app@latest ai-chat-app --typescript --tailwind --eslint --app
cd ai-chat-app

2. 必要パッケージのインストール

npm install ai @ai-sdk/openai @ai-sdk/react zod
npm install --save-dev @types/node

各パッケージの役割:

  • ai: Vercel AI SDKのコアライブラリ
  • @ai-sdk/openai: OpenAI APIプロバイダー
  • @ai-sdk/react: React専用フック
  • zod: 型安全なスキーマ検証

3. 環境変数の設定

.env.localファイルを作成し、OpenAI API Keyを設定:

# OpenAI API Key(https://platform.openai.com/api-keysで取得)
OPENAI_API_KEY=sk-your-openai-api-key

セキュリティ重要事項:

  • API Keyをフロントエンドに露出させない
  • 本番環境では環境変数管理サービスを使用
  • API使用量を監視して想定外のコストを防止

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

AI チャットボットの実装

1. API ルートの作成

app/api/chat/route.tsを作成:

import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
import { z } from 'zod';

// リクエストスキーマの定義
const chatRequestSchema = z.object({
  messages: z.array(z.object({
    role: z.enum(['user', 'assistant']),
    content: z.string().min(1).max(1000), // 入力長制限
  })),
});

export async function POST(req: Request) {
  try {
    const body = await req.json();
    
    // 入力値の検証
    const validatedData = chatRequestSchema.parse(body);
    const { messages } = validatedData;

    // OpenAI APIを使用したストリーミング応答
    const result = await streamText({
      model: openai('gpt-4-turbo'),
      messages,
      maxTokens: 500, // 応答長制限でコスト管理
      temperature: 0.7, // 応答の創造性調整
      // コンテンツフィルタリング
      tools: {},
    });

    return result.toAIStreamResponse();
  } catch (error) {
    console.error('Chat API Error:', error);
    
    // エラーハンドリング
    if (error instanceof z.ZodError) {
      return new Response(
        JSON.stringify({ error: 'Invalid request data' }),
        { status: 400 }
      );
    }

    return new Response(
      JSON.stringify({ error: 'Internal server error' }),
      { status: 500 }
    );
  }
}

2. チャットインターフェースコンポーネント

components/chat-interface.tsxを作成:

'use client';

import { useChat } from 'ai/react';
import { useState } from 'react';
import { Send, Loader2 } from 'lucide-react';

interface Message {
  id: string;
  role: 'user' | 'assistant';
  content: string;
}

export default function ChatInterface() {
  const [inputValue, setInputValue] = useState('');
  
  const {
    messages,
    input,
    handleInputChange,
    handleSubmit,
    isLoading,
    error
  } = useChat({
    api: '/api/chat',
    initialMessages: [
      {
        id: 'welcome',
        role: 'assistant',
        content: 'こんにちは!何についてお聞きしたいですか?'
      }
    ],
    // エラーハンドリング
    onError: (error) => {
      console.error('Chat error:', error);
    },
    // 応答完了時の処理
    onFinish: (message) => {
      console.log('Response completed:', message);
    }
  });

  return (
    <div className="flex flex-col h-screen max-w-4xl mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4 text-gray-800">
        AI Assistant
      </h1>
      
      {/* メッセージ表示エリア */}
      <div className="flex-1 overflow-y-auto mb-4 space-y-4 bg-gray-50 rounded-lg p-4">
        {messages.map((message) => (
          <div
            key={message.id}
            className={`flex ${
              message.role === 'user' ? 'justify-end' : 'justify-start'
            }`}
          >
            <div
              className={`max-w-xs lg:max-w-md px-4 py-2 rounded-lg ${
                message.role === 'user'
                  ? 'bg-blue-500 text-white'
                  : 'bg-white text-gray-800 border'
              }`}
            >
              {message.content}
            </div>
          </div>
        ))}
        
        {/* ローディング表示 */}
        {isLoading && (
          <div className="flex justify-start">
            <div className="bg-white text-gray-800 border px-4 py-2 rounded-lg flex items-center">
              <Loader2 className="h-4 w-4 animate-spin mr-2" />
              応答を生成中...
            </div>
          </div>
        )}
      </div>

      {/* エラー表示 */}
      {error && (
        <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
          エラーが発生しました: {error.message}
        </div>
      )}

      {/* 入力フォーム */}
      <form onSubmit={handleSubmit} className="flex gap-2">
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="メッセージを入力してください..."
          className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
          disabled={isLoading}
          maxLength={1000} // フロントエンドでも入力制限
        />
        <button
          type="submit"
          disabled={isLoading || !input.trim()}
          className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed flex items-center"
        >
          {isLoading ? (
            <Loader2 className="h-4 w-4 animate-spin" />
          ) : (
            <Send className="h-4 w-4" />
          )}
        </button>
      </form>
    </div>
  );
}

3. メインページの実装

app/page.tsxを更新:

import ChatInterface from '@/components/chat-interface';

export default function Home() {
  return (
    <main className="min-h-screen bg-gray-100">
      <ChatInterface />
    </main>
  );
}

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

React Server Componentsとの統合

1. サーバーサイドAI処理

app/api/summary/route.tsを作成:

import { openai } from '@ai-sdk/openai';
import { generateText } from 'ai';
import { z } from 'zod';

const summaryRequestSchema = z.object({
  text: z.string().min(1).max(5000),
});

export async function POST(req: Request) {
  try {
    const body = await req.json();
    const { text } = summaryRequestSchema.parse(body);

    // サーバーサイドでのAI処理
    const { text: summary } = await generateText({
      model: openai('gpt-4-turbo'),
      prompt: `以下のテキストを3行以内で要約してください:\n\n${text}`,
      maxTokens: 150,
    });

    return Response.json({ summary });
  } catch (error) {
    console.error('Summary API Error:', error);
    return new Response(
      JSON.stringify({ error: 'Summary generation failed' }),
      { status: 500 }
    );
  }
}

2. Server Componentでの事前処理

components/ai-summary.tsxを作成:

import { openai } from '@ai-sdk/openai';
import { generateText } from 'ai';

interface AISummaryProps {
  content: string;
}

// Server Componentとして実装
export default async function AISummary({ content }: AISummaryProps) {
  try {
    // サーバーサイドでAI処理を実行
    const { text: summary } = await generateText({
      model: openai('gpt-4-turbo'),
      prompt: `以下の内容の重要なポイントを3つ挙げてください:\n\n${content}`,
      maxTokens: 200,
    });

    return (
      <div className="bg-blue-50 p-4 rounded-lg border">
        <h3 className="font-semibold text-blue-800 mb-2">
          AI要約
        </h3>
        <p className="text-blue-700">{summary}</p>
      </div>
    );
  } catch (error) {
    console.error('AI Summary Error:', error);
    return (
      <div className="bg-red-50 p-4 rounded-lg border">
        <p className="text-red-700">要約の生成に失敗しました</p>
      </div>
    );
  }
}

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

高度な機能の実装

1. カスタムツール(Function Calling)

app/api/tools/route.tsを作成:

import { openai } from '@ai-sdk/openai';
import { streamText, tool } from 'ai';
import { z } from 'zod';

// 天気取得ツールの定義
const weatherTool = tool({
  description: '指定された都市の現在の天気を取得します',
  parameters: z.object({
    city: z.string().describe('都市名'),
  }),
  execute: async ({ city }) => {
    // 実際のAPIコールの代わりにモックデータを返す
    const mockWeather = {
      city,
      temperature: Math.floor(Math.random() * 30) + 5,
      condition: ['晴れ', '曇り', '雨', '雪'][Math.floor(Math.random() * 4)],
    };
    
    return `${city}の現在の天気: ${mockWeather.condition}、気温: ${mockWeather.temperature}°C`;
  },
});

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

    const result = await streamText({
      model: openai('gpt-4-turbo'),
      messages,
      tools: {
        getWeather: weatherTool,
      },
      toolChoice: 'auto', // 必要に応じてツールを使用
    });

    return result.toAIStreamResponse();
  } catch (error) {
    console.error('Tools API Error:', error);
    return new Response(
      JSON.stringify({ error: 'Tool execution failed' }),
      { status: 500 }
    );
  }
}

2. メモリ機能付きチャット

lib/chat-memory.tsを作成:

import { openai } from '@ai-sdk/openai';
import { generateText } from 'ai';

interface ChatMemory {
  userId: string;
  messages: Array<{
    role: 'user' | 'assistant';
    content: string;
    timestamp: Date;
  }>;
}

// メモリストレージ(本番環境ではデータベースを使用)
const memoryStore = new Map<string, ChatMemory>();

export class ChatMemoryManager {
  constructor(private userId: string) {}

  // 会話履歴の取得
  getHistory(): ChatMemory['messages'] {
    const memory = memoryStore.get(this.userId);
    return memory?.messages || [];
  }

  // メッセージの追加
  addMessage(role: 'user' | 'assistant', content: string) {
    const memory = memoryStore.get(this.userId) || {
      userId: this.userId,
      messages: [],
    };

    memory.messages.push({
      role,
      content,
      timestamp: new Date(),
    });

    // 直近20件のみ保持(メモリ使用量制限)
    if (memory.messages.length > 20) {
      memory.messages = memory.messages.slice(-20);
    }

    memoryStore.set(this.userId, memory);
  }

  // コンテキスト要約
  async summarizeContext(): Promise<string> {
    const messages = this.getHistory();
    if (messages.length < 5) return '';

    const conversationText = messages
      .map(m => `${m.role}: ${m.content}`)
      .join('\n');

    try {
      const { text } = await generateText({
        model: openai('gpt-4-turbo'),
        prompt: `以下の会話の要点を簡潔にまとめてください:\n\n${conversationText}`,
        maxTokens: 100,
      });

      return text;
    } catch (error) {
      console.error('Context summary error:', error);
      return '';
    }
  }
}

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

パフォーマンス最適化

1. 応答キャッシュシステム

lib/cache.tsを作成:

import { openai } from '@ai-sdk/openai';
import { generateText } from 'ai';

interface CacheEntry {
  response: string;
  timestamp: Date;
  expiresAt: Date;
}

class ResponseCache {
  private cache = new Map<string, CacheEntry>();
  private readonly TTL = 1000 * 60 * 30; // 30分

  // キャッシュキーの生成
  private generateKey(prompt: string): string {
    return Buffer.from(prompt).toString('base64');
  }

  // キャッシュから取得
  get(prompt: string): string | null {
    const key = this.generateKey(prompt);
    const entry = this.cache.get(key);

    if (!entry || entry.expiresAt < new Date()) {
      this.cache.delete(key);
      return null;
    }

    return entry.response;
  }

  // キャッシュに保存
  set(prompt: string, response: string): void {
    const key = this.generateKey(prompt);
    const now = new Date();
    
    this.cache.set(key, {
      response,
      timestamp: now,
      expiresAt: new Date(now.getTime() + this.TTL),
    });
  }

  // 期限切れエントリのクリーンアップ
  cleanup(): void {
    const now = new Date();
    for (const [key, entry] of this.cache.entries()) {
      if (entry.expiresAt < now) {
        this.cache.delete(key);
      }
    }
  }
}

export const responseCache = new ResponseCache();

// キャッシュ機能付きAI応答生成
export async function getCachedResponse(prompt: string): Promise<string> {
  // キャッシュから確認
  const cached = responseCache.get(prompt);
  if (cached) {
    return cached;
  }

  // AI応答生成
  const { text } = await generateText({
    model: openai('gpt-4-turbo'),
    prompt,
    maxTokens: 300,
  });

  // キャッシュに保存
  responseCache.set(prompt, text);
  return text;
}

// 定期的なクリーンアップ
setInterval(() => {
  responseCache.cleanup();
}, 1000 * 60 * 10); // 10分ごと

2. ストリーミング最適化

components/optimized-chat.tsxを作成:

'use client';

import { useChat } from 'ai/react';
import { useCallback, useMemo } from 'react';

export default function OptimizedChat() {
  const {
    messages,
    input,
    handleInputChange,
    handleSubmit,
    isLoading,
  } = useChat({
    api: '/api/chat',
    // ストリーミング最適化設定
    streamMode: 'stream-data',
    // 応答速度向上のための設定
    onFinish: useCallback((message) => {
      // 応答完了時の軽量処理
      console.log('Message completed');
    }, []),
  });

  // メッセージのメモ化
  const memoizedMessages = useMemo(() => messages, [messages]);

  return (
    <div className="flex flex-col h-screen">
      {/* 仮想化されたメッセージリスト */}
      <div className="flex-1 overflow-y-auto">
        {memoizedMessages.map((message) => (
          <div key={message.id} className="p-2">
            {message.content}
          </div>
        ))}
      </div>
      
      {/* 最適化された入力フォーム */}
      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={handleInputChange}
          disabled={isLoading}
          className="w-full p-2 border rounded"
        />
      </form>
    </div>
  );
}

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

セキュリティ強化

1. レート制限の実装

middleware.tsを作成:

import { NextRequest, NextResponse } from 'next/server';

// レート制限ストレージ
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();

const RATE_LIMIT_WINDOW = 60 * 1000; // 1分
const MAX_REQUESTS = 10; // 1分間に10リクエスト

export function middleware(request: NextRequest) {
  // API routesのみに適用
  if (!request.nextUrl.pathname.startsWith('/api/')) {
    return NextResponse.next();
  }

  const clientId = request.ip || 'anonymous';
  const now = Date.now();
  
  const clientData = rateLimitStore.get(clientId);
  
  if (!clientData || now > clientData.resetTime) {
    // 新しいウィンドウの開始
    rateLimitStore.set(clientId, {
      count: 1,
      resetTime: now + RATE_LIMIT_WINDOW,
    });
    return NextResponse.next();
  }

  if (clientData.count >= MAX_REQUESTS) {
    return new NextResponse(
      JSON.stringify({ error: 'Rate limit exceeded' }),
      { status: 429 }
    );
  }

  // カウント増加
  clientData.count++;
  rateLimitStore.set(clientId, clientData);
  
  return NextResponse.next();
}

export const config = {
  matcher: '/api/:path*',
};

2. 入力サニタイゼーション

lib/security.tsを作成:

import DOMPurify from 'isomorphic-dompurify';

// XSS対策のための入力サニタイゼーション
export function sanitizeInput(input: string): string {
  // HTMLタグの除去
  const cleanInput = DOMPurify.sanitize(input, { ALLOWED_TAGS: [] });
  
  // 長さ制限
  return cleanInput.slice(0, 1000);
}

// プロンプトインジェクション対策
export function validatePrompt(prompt: string): boolean {
  const dangerousPatterns = [
    /ignore.*previous.*instructions/i,
    /system.*prompt/i,
    /act.*as.*different/i,
    /pretend.*you.*are/i,
  ];

  return !dangerousPatterns.some(pattern => pattern.test(prompt));
}

// APIキーの検証
export function validateApiKey(key: string): boolean {
  return key.startsWith('sk-') && key.length > 20;
}

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

エラーハンドリング

1. 包括的エラーハンドリング

lib/error-handler.tsを作成:

export class AIError extends Error {
  constructor(
    message: string,
    public code: string,
    public statusCode: number = 500
  ) {
    super(message);
    this.name = 'AIError';
  }
}

export class RateLimitError extends AIError {
  constructor() {
    super('Rate limit exceeded', 'RATE_LIMIT', 429);
  }
}

export class ValidationError extends AIError {
  constructor(message: string) {
    super(message, 'VALIDATION_ERROR', 400);
  }
}

export function handleAPIError(error: unknown): Response {
  console.error('API Error:', error);

  if (error instanceof AIError) {
    return new Response(
      JSON.stringify({
        error: error.message,
        code: error.code,
      }),
      { status: error.statusCode }
    );
  }

  // 予期しないエラー
  return new Response(
    JSON.stringify({
      error: 'Internal server error',
      code: 'INTERNAL_ERROR',
    }),
    { status: 500 }
  );
}

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

デプロイと運用

1. Vercelデプロイ設定

vercel.jsonを作成:

{
  "functions": {
    "app/api/chat/route.ts": {
      "maxDuration": 30
    }
  },
  "env": {
    "OPENAI_API_KEY": "@openai-api-key"
  }
}

2. 環境別設定

.env.exampleを作成:

# OpenAI Configuration
OPENAI_API_KEY=sk-your-openai-api-key
OPENAI_MODEL=gpt-4-turbo

# Application Configuration
NODE_ENV=production
NEXT_PUBLIC_APP_URL=https://your-app.vercel.app

# Rate Limiting
RATE_LIMIT_REQUESTS=10
RATE_LIMIT_WINDOW=60000

# Monitoring
SENTRY_DSN=your-sentry-dsn

3. 監視とログ設定

lib/monitoring.tsを作成:

export class AIMonitoring {
  static logAPIUsage(endpoint: string, tokens: number, duration: number) {
    console.log(`API Usage - Endpoint: ${endpoint}, Tokens: ${tokens}, Duration: ${duration}ms`);
    
    // 本番環境では監視サービスに送信
    if (process.env.NODE_ENV === 'production') {
      // Sentry, DataDog等への送信
    }
  }

  static logError(error: Error, context: Record<string, unknown>) {
    console.error('AI Application Error:', {
      message: error.message,
      stack: error.stack,
      context,
    });
  }
}

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

本番運用のベストプラクティス

1. コスト管理

// app/api/chat/route.ts(コスト最適化版)
export async function POST(req: Request) {
  try {
    const startTime = Date.now();
    
    const result = await streamText({
      model: openai('gpt-4-turbo'),
      messages,
      maxTokens: 300, // コスト制限
      temperature: 0.3, // 一貫性重視でコスト削減
    });

    // 使用量記録
    const duration = Date.now() - startTime;
    AIMonitoring.logAPIUsage('/api/chat', 300, duration);

    return result.toAIStreamResponse();
  } catch (error) {
    AIMonitoring.logError(error as Error, { endpoint: '/api/chat' });
    return handleAPIError(error);
  }
}

2. パフォーマンス監視

// components/performance-monitor.tsx
'use client';

import { useEffect } from 'react';

export default function PerformanceMonitor() {
  useEffect(() => {
    // First Contentful Paint測定
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries();
      entries.forEach((entry) => {
        if (entry.name === 'first-contentful-paint') {
          console.log('FCP:', entry.startTime);
        }
      });
    });

    observer.observe({ entryTypes: ['paint'] });

    return () => observer.disconnect();
  }, []);

  return null;
}

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

まとめ

本記事では、Next.js 15とTypeScriptを使用したAI統合Webアプリケーションの構築方法を包括的に解説しました。

実装のポイント:

  1. Vercel AI SDKの活用による効率的な開発
  2. React Server Componentsでのパフォーマンス最適化
  3. セキュリティ対策の徹底実装
  4. エラーハンドリングとモニタリング
  5. 本番運用を想定した設計

次のステップ:

  • 複数AIプロバイダーの統合
  • ベクターデータベースによるRAG実装
  • リアルタイム協調編集機能
  • 高度なプロンプトエンジニアリング

2025年のAI統合開発は、単なる機能実装から「ユーザー体験の向上」「運用効率の改善」「コスト最適化」を重視した戦略的アプローチが求められます。本記事のコードをベースに、あなたのプロダクトに最適なAI統合を実現してください。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

この記事をシェア

続けて読みたい記事

編集部がピックアップした関連記事で学びを広げましょう。

#TypeScript

TypeScript企業導入の実践的移行戦略:チーム運用とROI最大化の完全ガイド【2025年最新】

2025/8/11
#Python

PythonだけでモダンなWebアプリが作れる!Reflex入門ガイド【2025年最新】

2025/11/26
#ベクターデータベース

ベクターデータベースで構築するセマンティック検索システム完全ガイド【2025年最新】

2025/8/12
#Apache Spark

Apache SparkとKafkaで構築するリアルタイムデータパイプライン完全ガイド【2025年最新】

2025/8/12
#WebGPU

WebGPUで動くブラウザ完結LLM実装ガイド【2025年最新】

2025/11/26
#Security

Secrets/環境変数の実践ガイド【2025年版】:Next.js/Node/CI/CDの安全な管理

2025/9/13