Twilio Messaging API のデモ用アプリを Vercel でサクッとデプロイしてみた

Twilio Messaging API のデモ用アプリを Vercel でサクッとデプロイしてみた

Twilio の Messaging API を使用した SMS 受信デモアプリを Next.js で作成し、 Vercel にデプロイする手順を解説します。Twilio Webhook の設定から Vercel でのデプロイまで、実際の動作確認を含めて詳しく説明します。
2025.07.18

はじめに

Twilio の Messaging 機能をデモンストレーションするため、iOS 風の SMS 受信 UI を Vercel でデプロイしました。本記事では、その実装内容を紹介します。

動作イメージ

Twilio とは

Twilio は、 SMS 、音声通話、ビデオ通話などの通信機能を API で提供するクラウドサービスです。開発者は簡単に通信機能をアプリケーションに組み込むことができます。今回は Messaging API を使用して SMS の受信機能を実装します。

Vercel とは

Vercel は、フロントエンドアプリケーションのデプロイとホスティングに特化したプラットフォームです。 Next.js で作成したアプリを Git リポジトリと連携して簡単にデプロイすることが可能です。

対象読者

  • Next.js の基本的な知識がある方
  • Twilio の Messaging API に興味がある方
  • Vercel の活用事例が知りたい方

参考

プロジェクトセットアップ

まず、 Next.js プロジェクトを作成し、必要な依存関係をインストールします。

			
			npx create-next-app@latest sms-receiver-app --typescript --tailwind --eslint --app
cd sms-receiver-app
npm install framer-motion

		

Twilio Webhook エンドポイントの実装

app/api/sms/route.ts に、 Twilio からの webhook を受け取り、クライアントに配信するエンドポイントを実装します。

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

// SSE 接続中のクライアントを管理するセット
const clients = new Set<ReadableStreamDefaultController>()

/**
 * SSE (Server-Sent Events) エンドポイント
 * クライアントからの GET リクエストで SSE 接続を確立
 */
export async function GET() {
    const stream = new ReadableStream({
        start(controller) {
            // 新しいクライアントを接続リストに追加
            clients.add(controller)
            // 接続確認メッセージを送信
            controller.enqueue(`data: {"type":"connected"}\n\n`)
        },
        cancel(controller) {
            // 接続が切断された際にクライアントをリストから削除
            clients.delete(controller)
        }
    })

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

/**
 * Twilio Webhook エンドポイント
 * SMS 受信時に Twilio から POST リクエストが送信される
 */
export async function POST(request: NextRequest) {
    try {
        // Twilio からの form データを取得
        const formData = await request.formData()

        // Vercel は UTC 環境のため、日本時間に変換
        const now = new Date()
        const japanTime = new Date(now.toLocaleString("en-US", {timeZone: "Asia/Tokyo"}))

        // メッセージオブジェクトを構築
        const message = {
            from: formData.get('From') as string,
            body: formData.get('Body') as string,
            time: japanTime.toLocaleTimeString('ja-JP', {
                hour: '2-digit',
                minute: '2-digit',
                hour12: false
            })
        }

        console.log('SMS received at:', now.toISOString(), 'Japan time:', message.time)

        // 接続中の全クライアントにメッセージを配信
        const data = `data: ${JSON.stringify({ type: 'message', ...message })}\n\n`
        clients.forEach(controller => {
            try {
                controller.enqueue(data)
            } catch {
                // 送信に失敗したクライアントは接続リストから削除
                clients.delete(controller)
            }
        })

        // Twilio に対する XML レスポンスを返却
        return new NextResponse(
            '<?xml version="1.0" encoding="UTF-8"?><Response></Response>',
            { headers: { 'Content-Type': 'text/xml' } }
        )
    } catch (error) {
        console.error('Webhook error:', error)
        return NextResponse.json({ error: 'Error' }, { status: 500 })
    }
}

		

フロントエンド UI の実装

app/page.tsx に、 iOS 風の SMS 受信画面を実装します。

			
			'use client'

import { useEffect, useState } from 'react'
import { motion } from 'framer-motion'

/**
 * SMS メッセージの型定義
 */
interface Message {
    from: string
    body: string
    time: string
}

/**
 * SMS 受信デモ用の iOS 風 UI コンポーネント
 */
export default function SMSDemo() {
    const [message, setMessage] = useState<Message | null>(null)
    const [connected, setConnected] = useState(false)
    const [currentTime, setCurrentTime] = useState('')

    useEffect(() => {
        /**
         * ステータスバーの現在時刻を更新する関数
         */
        const updateTime = () => {
            const now = new Date()
            setCurrentTime(now.toLocaleTimeString('ja-JP', {
                hour: '2-digit',
                minute: '2-digit',
                hour12: false
            }))
        }

        // 初回実行と1秒ごとの時刻更新を設定
        updateTime()
        const timeInterval = setInterval(updateTime, 1000)

        // SSE 接続を確立してリアルタイムメッセージを受信
        const eventSource = new EventSource('/api/sms')

        eventSource.onmessage = (event) => {
            const data = JSON.parse(event.data)

            if (data.type === 'connected') {
                // サーバーとの接続が確立された場合
                setConnected(true)
            } else if (data.type === 'message') {
                // 新しい SMS メッセージを受信した場合
                setMessage({
                    from: data.from,
                    body: data.body,
                    time: data.time
                })
            }
        }

        // クリーンアップ処理
        return () => {
            clearInterval(timeInterval)
            eventSource.close()
        }
    }, [])

    return (
        <div className="min-h-screen bg-black flex flex-col max-w-sm mx-auto">
            {/* iOS 風ステータスバー */}
            <div className="bg-black text-white text-sm px-4 py-2 flex justify-between">
                <span>{currentTime}</span>
                {/* 接続状態インジケーター */}
                <div className={`w-2 h-2 rounded-full ${connected ? 'bg-green-400' : 'bg-red-400'}`}></div>
            </div>

            {/* アプリヘッダー */}
            <div className="bg-gray-50 px-4 py-4 border-b">
                <h1 className="text-lg font-semibold text-center">メッセージ</h1>
            </div>

            {/* メッセージ表示エリア */}
            <div className="flex-1 bg-white p-4">
                {message ? (
                    // メッセージ受信時のアニメーション付き表示
                    <motion.div
                        initial={{ opacity: 0, y: 30, scale: 0.8 }}
                        animate={{ opacity: 1, y: 0, scale: 1 }}
                        transition={{
                            duration: 0.8,
                            ease: [0.25, 0.46, 0.45, 0.94],
                            scale: { type: "spring", damping: 15, stiffness: 200 }
                        }}
                    >
                        {/* 送信者情報 */}
                        <div className="text-xs text-gray-500 mb-2">{message.from}</div>

                        {/* メッセージバブル */}
                        <motion.div
                            className="bg-gray-100 rounded-2xl px-4 py-3 max-w-xs shadow-sm"
                            // 受信時の光るエフェクト
                            initial={{ boxShadow: "0 0 0 rgba(59, 130, 246, 0)" }}
                            animate={{
                                boxShadow: [
                                    "0 0 0 rgba(59, 130, 246, 0)",
                                    "0 0 20px rgba(59, 130, 246, 0.3)",
                                    "0 0 0 rgba(59, 130, 246, 0)"
                                ]
                            }}
                            transition={{ duration: 2 }}
                        >
                            <p className="text-sm">{message.body}</p>
                            <div className="text-xs text-gray-400 mt-2 text-right">{message.time}</div>
                        </motion.div>
                    </motion.div>
                ) : (
                    // メッセージ待機中の表示
                    <div className="flex items-center justify-center h-full text-center">
                        <div>
                            {/* メッセージアイコン */}
                            <div className="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
                                <svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
                                </svg>
                            </div>
                            <p className="text-gray-500 text-sm">SMS の受信を待機中...</p>
                        </div>
                    </div>
                )}
            </div>
        </div>
    )
}

		

ローカルでの動作確認

下記コマンドを実行してローカル環境で正しく動作することを確認します。

			
			npm run dev

		

http://localhost:3000 にアクセスし、アプリ画面を開きます。

ローカルでの実行結果

curl コマンドで sms 送信をシミュレートし、画面に表示が浮かぶのを確認します。

			
			curl -X POST http://localhost:3000/api/sms \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "From=%2B819012345678&Body=テストメッセージです"
<?xml version="1.0" encoding="UTF-8"?><Response></Response>koshiitakumi@HL01580:~/work/sms-receiver-app$ curl -X POST http://localhost:3000/api/sms   -H "Content-Type: application/x-www-form-urlencoded"   -d "From=%2B819012345678&Body=テストメッセージです"
<?xml version="1.0" encoding="UTF-8"?><Response></Response>

		

curl コマンド実行結果

Vercel へのデプロイ

GitHub リポジトリにプッシュします。リポジトリは private でもかまいません。

			
			git add .
git commit -m "Initial commit"
git push origin main

		

github のリポジトリ

Overview > Add New... > Project より新規のプロジェクトを作成します。

Add New ボタン

作成したリポジトリをインポートします。

Import ボタン

Framework Preset に Next.js が選ばれていることを確認し、デプロイします。

Deploy ボタン

デプロイが完了すると次のような画面となります。

デプロイ完了画面

https://your-app.vercel.app のような URL が発行されるので控えておきます。

デプロイ後のプロジェクト画面

Twilio の設定

Twilio Console で以下の設定を行います。

  1. Phone Numbers > Manage > Active numbers で、SMS を受信する電話番号をクリック
  2. Messaging Configuraton セクションで以下を設定
    • A message comes in: Webhook
    • URL: 先に控えた URL + /api/sms (例: https://your-app.vercel.app/api/sms)
    • HTTP: POST
  3. Save configuration をクリック

Twilio 設定

Vercel 環境での動作確認

ブラウザで先に控えた URL (例: https://your-app.vercel.app) にアクセスします。

Vercel 環境のアプリ画面

SMS を送信し、メッセージが表示されることを確認します。

メッセージ受信デモ

まとめ

Twilio Webhook の仕組みと Vercel を組み合わせることで、簡単に SMS 受信デモアプリをデプロイできました。この実装は Twilio API のデモンストレーションや SMS 通知システムのプロトタイプとして活用が期待できます。

Share this article

FacebookHatena blogX

Related articles

Twilio Messaging API のデモ用アプリを Vercel でサクッとデプロイしてみた | DevelopersIO