Claude APIAI開発TypeScriptNext.jsVercel

Claude APIでAIチャットアプリを2日で構築した実装記録

読了約 8

AnthropicのClaude APIは、GPT系のモデルと比較してコンテキストの理解力が高く、長文の指示に対しても忠実に応答してくれます。今回は週末の2日間を使い、Claude APIを活用したAIチャットアプリをゼロから構築しました。この記事では、APIのセットアップからストリーミング実装、プロンプトエンジニアリング、エラーハンドリング、Vercelへのデプロイまでの全工程を実際のコードとともに解説します。

Claude APIのセットアップ

まず、Anthropicの公式サイト(console.anthropic.com)でアカウントを作成し、APIキーを発行します。APIキーは「sk-ant-」から始まる文字列で、絶対にソースコードに直書きしてはいけません。環境変数として管理します。

SDKのインストールはnpmで行います。2024年後半からAnthropicはTypeScript SDKを大幅に改善しており、型補完が非常に充実しています。

bash
# SDKのインストール
npm install @anthropic-ai/sdk

# 環境変数ファイルの作成
echo "ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxx" > .env.local

環境変数はNext.jsの場合、サーバーサイドのAPIルートからのみアクセスします。クライアントコンポーネントからAPIキーを直接呼び出すと、ブラウザ経由で漏洩するリスクがあります。

ストリーミングレスポンスの実装

チャットアプリの体験を大きく左右するのがストリーミングです。通常のレスポンスはAPIが全文を生成し終わるまでユーザーは待ち続けますが、ストリーミングを使うと文字が流れるように表示されます。Claude APIはServer-Sent Events(SSE)形式のストリーミングに対応しています。

typescript
// app/api/chat/route.ts
import Anthropic from '@anthropic-ai/sdk'
import { NextRequest } from 'next/server'

const client = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY,
})

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

  const encoder = new TextEncoder()

  const stream = new ReadableStream({
    async start(controller) {
      try {
        const response = await client.messages.create({
          model: 'claude-opus-4-5',
          max_tokens: 1024,
          stream: true,
          messages,
        })

        for await (const event of response) {
          if (
            event.type === 'content_block_delta' &&
            event.delta.type === 'text_delta'
          ) {
            controller.enqueue(
              encoder.encode(`data: ${JSON.stringify({ text: event.delta.text })}\n\n`)
            )
          }
        }

        controller.enqueue(encoder.encode('data: [DONE]\n\n'))
        controller.close()
      } catch (error) {
        controller.error(error)
      }
    },
  })

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive',
    },
  })
}

クライアント側では、fetch APIのReaderを使ってストリームを読み取ります。React Stateと組み合わせることで、テキストが流れるようなUXを実現できます。

typescript
// components/ChatInterface.tsx
'use client'

import { useState } from 'react'

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

export function ChatInterface() {
  const [messages, setMessages] = useState<Message[]>([])
  const [input, setInput] = useState('')
  const [isStreaming, setIsStreaming] = useState(false)

  const sendMessage = async () => {
    if (!input.trim() || isStreaming) return

    const userMessage: Message = { role: 'user', content: input }
    const newMessages = [...messages, userMessage]
    setMessages(newMessages)
    setInput('')
    setIsStreaming(true)

    // アシスタントメッセージの枠を先に追加
    setMessages((prev) => [...prev, { role: 'assistant', content: '' }])

    try {
      const res = await fetch('/api/chat', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ messages: newMessages }),
      })

      const reader = res.body?.getReader()
      const decoder = new TextDecoder()

      if (!reader) return

      while (true) {
        const { done, value } = await reader.read()
        if (done) break

        const chunk = decoder.decode(value)
        const lines = chunk.split('\n').filter((l) => l.startsWith('data: '))

        for (const line of lines) {
          const data = line.replace('data: ', '')
          if (data === '[DONE]') break

          const parsed = JSON.parse(data) as { text: string }
          setMessages((prev) => {
            const updated = [...prev]
            updated[updated.length - 1] = {
              role: 'assistant',
              content: updated[updated.length - 1].content + parsed.text,
            }
            return updated
          })
        }
      }
    } finally {
      setIsStreaming(false)
    }
  }

  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((msg, i) => (
          <div
            key={i}
            className={`p-3 rounded-lg ${
              msg.role === 'user'
                ? 'bg-blue-100 ml-8'
                : 'bg-gray-100 mr-8'
            }`}
          >
            {msg.content}
          </div>
        ))}
      </div>
      <div className="flex gap-2">
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && sendMessage()}
          className="flex-1 border rounded-lg px-3 py-2"
          placeholder="メッセージを入力..."
        />
        <button
          onClick={sendMessage}
          disabled={isStreaming}
          className="bg-blue-600 text-white px-4 py-2 rounded-lg disabled:opacity-50"
        >
          送信
        </button>
      </div>
    </div>
  )
}

プロンプトエンジニアリングの基本

Claude APIを効果的に使うには、systemプロンプトの設計が重要です。Claude 3以降のモデルはsystemプロンプトをとても忠実に守ります。以下のポイントを押さえることで、アプリの品質が大幅に向上します。

  • ペルソナを明確に定義する(例:「あなたはTypeScriptの専門家エンジニアです」)
  • 出力フォーマットを具体的に指定する(JSON形式、マークダウン形式など)
  • 禁止事項をリストアップする(例:「架空の情報を作らない」)
  • few-shotサンプルを提供する(理想の入出力ペアを2〜3個)
  • 思考の連鎖(Chain of Thought)を促す(例:「ステップバイステップで考えてください」)

エラーハンドリングとレート制限

Claude APIには1分あたりのリクエスト数とトークン数に制限があります。無料枠ではAPIコールが少ないため、本格的なアプリでは有料プランへの移行が必要です。エラーが発生した際の適切なハンドリングも重要です。

typescript
// lib/claude-client.ts
import Anthropic from '@anthropic-ai/sdk'

const client = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY,
  maxRetries: 3,  // 自動リトライ(デフォルト2回)
  timeout: 60000, // 60秒タイムアウト
})

export async function callClaude(
  messages: Anthropic.MessageParam[],
  systemPrompt?: string
): Promise<string> {
  try {
    const response = await client.messages.create({
      model: 'claude-opus-4-5',
      max_tokens: 2048,
      system: systemPrompt,
      messages,
    })

    const content = response.content[0]
    if (content.type !== 'text') {
      throw new Error('Unexpected response type')
    }

    return content.text
  } catch (error) {
    if (error instanceof Anthropic.APIError) {
      // レート制限エラー
      if (error.status === 429) {
        throw new Error('APIのレート制限に達しました。しばらく後にお試しください。')
      }
      // 認証エラー
      if (error.status === 401) {
        throw new Error('APIキーが無効です。')
      }
      // その他のAPIエラー
      throw new Error(`APIエラー: ${error.message}`)
    }
    throw error
  }
}

⚠️ 注意

APIキーは絶対にGitHubなどのパブリックリポジトリにコミットしないでください。誤ってコミットした場合は、Anthropicコンソールで即座に無効化し、新しいキーを発行してください。

Vercelへのデプロイ

Next.jsアプリのデプロイ先としてVercelは最適です。GitHubリポジトリと連携すれば、プッシュのたびに自動デプロイされます。環境変数はVercelダッシュボードの「Settings → Environment Variables」から設定します。

  1. 1Vercelアカウントを作成し、GitHubと連携する
  2. 2リポジトリをインポートし、プロジェクトを作成する
  3. 3Settings → Environment Variables で ANTHROPIC_API_KEY を追加する
  4. 4Deployボタンを押して本番デプロイ完了

Vercel Hobbyプランは個人プロジェクトに十分なスペックを提供しています。APIルートはServerless Functionsとして実行され、コールドスタートも考慮した設計が必要です。

実装で学んだ教訓

2日間でのプロトタイプ開発を通じて、いくつかの重要な知見を得ました。特にストリーミングの実装は初めてだったため、SSEの仕様を理解するのに時間がかかりました。しかし一度理解すれば、他のAI APIでも応用できる汎用的なスキルです。

  • システムプロンプトの設計はアプリ品質の7割を決める
  • ストリーミングはUXを劇的に改善するが実装コストは高くない
  • エラーハンドリングは最初から丁寧に実装する
  • APIのレスポンス速度はモデルサイズに大きく依存する(Haikuが最速)
  • コストはmax_tokensとmodel選択で大幅に変わる

💡 ヒント

Claudeのモデルは用途に合わせて選択しましょう。claude-haiku-4-5はコスト最優先、claude-sonnet-4-5はバランス型、claude-opus-4-5は最高品質が必要な場合に使います。プロトタイプ段階ではHaikuで動作確認し、本番でSonnetに切り替えるのがおすすめです。

Claude APIAI開発TypeScriptNext.jsVercel

AI協働開発のご相談はこちら

この記事の内容を実際のプロジェクトに活用したい方、 開発のご依頼・ご質問はお気軽にどうぞ。

無料相談を申し込む