✍️

Next.js×Python連携の無料最適解: Vercel+Render.comで無料デプロイする方法

に公開

Next.js×Pythonの無料最適解 - Vercel+Render.comの連携

はじめに

nemuriです。
現在100日チャレンジをしていて、その結果をポートフォリオにして公開して5日目です。
今後pythonで書いたコードを表示したくなるだろうと思い、今日はその実装をしました。

フロントエンドにNext.js、バックエンドにPythonを使うアプリケーションを無料でデプロイする方法を知りたくありませんか?

本記事では、「NumPy計算機」の実装を通して、Vercel + Render.comを組み合わせたデプロイ手法を紹介します。

背景: 直面した課題

当初の目標はVercel単体でNext.jsフロントエンドとPython (NumPy) バックエンドを両方デプロイすることでした。しかし、以下の問題が発生しました:

  • Vercelのvercel.jsonでPythonを設定してもビルドが失敗
  • requirements.txtを追加しても依存関係の解決に問題発生
  • GitHub Checksでビルド失敗警告が出続ける
  • ESLintエラーとTypeScriptの混在問題

解決策: アーキテクチャを変更し、各技術に最適化されたプラットフォームで分離デプロイする方針に転換しました。

実装方針: 2つのサービスの連携

最終的に採用したアーキテクチャは以下の通りです:

  1. フロントエンド (Next.js): Vercelにデプロイ

    • ユーザーインターフェース (入力フォーム、結果表示)
    • API通信処理
    • スタイリング (Tailwind CSS)
  2. バックエンド (Python/FastAPI): Render.comにデプロイ

    • /calculate エンドポイント
    • NumPyによる統計計算処理
    • CORS対応

プロジェクト構成

このプロジェクトは、2つの独立したサービスとして管理されています:

1. Next.js(Vercel)フロントエンド側

my-portfolio/
├── src/
│   ├── app/
│       ├── challenges/
│           └── day5/
│               └── page.tsx      # NumPy計算フォーム&結果表示ページ
├── vercel.json                   # Vercel用設定ファイル
└── ...(その他Next.js標準ファイル)

2. Python APIサーバー(Render.com用)

python-api/
├── main.py                # FastAPI本体(NumPy計算APIのエンドポイント実装)
├── requirements.txt       # Python依存パッケージリスト
└── Dockerfile             # Render.com用Docker設定

全体構成図

my-portfolio/           # Next.js (Vercel) プロジェクト
│
├── src/
│   └── ...             # Next.jsの各種ファイル
├── vercel.json
└──python-api/             # FastAPI (Render.com) プロジェクト
    ├── ...
└── ...(他Next.js標準ファイル)


この構成の重要なポイント:

  • Vercel側にはPython関連ファイルは不要
  • Render.com用APIサーバーは別ディレクトリで管理
  • APIエンドポイントは /calculate などに統一
  • フロントエンドからは Render.com のAPI URLをfetchで呼び出す

実装手順: ステップバイステップで解説

1. FastAPI (Python) バックエンドの構築

まず、NumPy計算を行うPython APIサーバーを構築します。(main.py)

# main.py
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
import numpy as np
from typing import List
from pydantic import BaseModel

app = FastAPI()

# CORSの設定
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 本番環境では適切なオリジンを指定してください
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

class Numbers(BaseModel):
    numbers: List[float]

@app.get("/")
async def root():
    return {"message": "NumPy Calculator API is running"}

@app.post("/calculate")
async def calculate(numbers: Numbers):
    try:
        # 数値の配列をNumPy配列に変換
        arr = np.array(numbers.numbers)
        
        # 基本的な統計計算を実行
        result = {
            'mean': float(np.mean(arr)),
            'median': float(np.median(arr)),
            'std': float(np.std(arr)),
            'sum': float(np.sum(arr))
        }
        
        return {"result": result}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e)) 

依存パッケージを指定(requirements.txt)

# requirements.txt
fastapi==0.104.1
uvicorn==0.23.2
numpy==1.24.3
pydantic==2.4.2

Render.com用のDockerfileを作成(Dodkerfile)

FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "10000"]

2. Render.comにPythonバックエンドをデプロイ

  1. Render.comにアカウント登録
  2. 「Web Service」を選択
  3. GitHubリポジトリと連携(または手動デプロイ)
  4. 以下の設定で新規サービスを作成:
    • 名前: my-portfolio(任意)
    • 環境: Docker
    • ビルドコマンド: 自動検出
    • スタートコマンド: 自動検出
    • プラン: Free

デプロイが成功すると、https://my-portfolio-xxxxx.onrender.com/ のようなURLが発行されます。

3. Next.jsフロントエンドの実装

続いて、NumPy計算機のUIを実装します(page.tsx)

// src/app/challenges/day5/page.tsx
'use client';

import { useState } from 'react';
import Header from '@/components/Header';
import Link from 'next/link';

interface CalculationResult {
  mean: number;
  median: number;
  std: number;
  sum: number;
}

export default function CalculatePage() {
  const [numbers, setNumbers] = useState('');
  const [result, setResult] = useState<CalculationResult | null>(null);
  const [error, setError] = useState('');
  const [isLoading, setIsLoading] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsLoading(true);
    setError('');
    setResult(null);

    try {
      // 入力された文字列を数値の配列に変換
      const numberArray = numbers.split(',').map(n => parseFloat(n.trim()));

      // Python APIサーバーにリクエストを送信
      const response = await fetch('https://my-portfolio-a30e.onrender.com/calculate', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ numbers: numberArray }),
      });

      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.detail || '計算中にエラーが発生しました');
      }

      setResult(data.result);
    } catch (err) {
      setError(err instanceof Error ? err.message : '予期せぬエラーが発生しました');
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="min-h-screen bg-gray-50 dark:bg-gray-900">
      <Header />
      <main className="pt-16">
        <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
          <div className="flex items-center gap-4 mb-8">
            <Link 
              href="/challenges" 
              className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors"
            >
              ← Back to Challenges
            </Link>
          </div>
          <div className="max-w-md mx-auto">
            <h1 className="text-4xl font-bold text-center mb-8 text-gray-900 dark:text-white">
              NumPy計算機
            </h1>
            
            <div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-lg">
              <form onSubmit={handleSubmit} className="space-y-6">
                <div>
                  <label 
                    htmlFor="numbers" 
                    className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
                  >
                    数値をカンマ区切りで入力してください
                  </label>
                  <input
                    type="text"
                    id="numbers"
                    value={numbers}
                    onChange={(e) => setNumbers(e.target.value)}
                    placeholder="例: 1, 2, 3, 4, 5"
                    className="w-full p-3 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
                    required
                  />
                </div>

                <button
                  type="submit"
                  disabled={isLoading}
                  className="w-full bg-blue-500 hover:bg-blue-600 text-white font-medium py-3 px-4 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
                >
                  {isLoading ? '計算中...' : '計算実行'}
                </button>
              </form>

              {error && (
                <div className="mt-6 p-4 bg-red-100 dark:bg-red-900/50 text-red-800 dark:text-red-200 rounded-md">
                  {error}
                </div>
              )}

              {result && (
                <div className="mt-6 p-6 bg-gray-50 dark:bg-gray-700/50 rounded-md">
                  <h2 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
                    計算結果
                  </h2>
                  <dl className="space-y-3">
                    <div className="flex justify-between items-center">
                      <dt className="font-medium text-gray-700 dark:text-gray-300">平均値:</dt>
                      <dd className="text-gray-900 dark:text-white">{result.mean.toFixed(2)}</dd>
                    </div>
                    <div className="flex justify-between items-center">
                      <dt className="font-medium text-gray-700 dark:text-gray-300">中央値:</dt>
                      <dd className="text-gray-900 dark:text-white">{result.median.toFixed(2)}</dd>
                    </div>
                    <div className="flex justify-between items-center">
                      <dt className="font-medium text-gray-700 dark:text-gray-300">標準偏差:</dt>
                      <dd className="text-gray-900 dark:text-white">{result.std.toFixed(2)}</dd>
                    </div>
                    <div className="flex justify-between items-center">
                      <dt className="font-medium text-gray-700 dark:text-gray-300">合計:</dt>
                      <dd className="text-gray-900 dark:text-white">{result.sum.toFixed(2)}</dd>
                    </div>
                  </dl>
                </div>
              )}
            </div>
          </div>
        </div>
      </main>
    </div>
  );
}

4. Vercelにフロントエンドをデプロイ

Vercel側のビルド設定を整理し、Python関連の設定を削除します。(vercel.json)

// vercel.json
{
  "version": 2,
  "builds": [
    {
      "src": "package.json",
      "use": "@vercel/next"
    }
  ]
} 

実装上の重要ポイント

1. CORS対策の実装

FastAPIでCORSを適切に設定しないと、ブラウザからのリクエストがブロックされます。CORSMiddlewareを正しく設定することが重要です。

開発中はallow_origins=["*"]で全てのオリジンを許可しても良いですが、本番環境では特定のドメインのみに制限しましょう:

app.add_middleware(
    CORSMiddleware,
    allow_origins=["ここにURLを入れる"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

2. エラーハンドリングの強化

フロントエンドとバックエンドの両方でエラーハンドリングを実装することで、ユーザー体験が向上します:

try {
  // APIリクエスト処理...
  
  if (!response.ok) {
    const errorData = await response.json();
    throw new Error(errorData.detail || 'デフォルトエラーメッセージ');
  }
  
  // 成功時の処理...
} catch (err) {
  // エラー処理...
}

3. 型安全性の確保

TypeScriptとPydanticを活用して、フロントエンドとバックエンドの両方で型安全性を確保しています:

// TypeScript側
interface CalculationResult {
  mean: number;
  median: number;
  std: number;
  sum: number;
}
# Python側(Pydantic)
class CalculationResult(BaseModel):
    mean: float
    median: float
    std: float
    sum: float

無料デプロイの制限事項と対策

次の制限を理解しておくと良いでしょう:

1. Render.com無料プランの制限
- 使用されていない場合、15分間アイドル状態になるとサービスがスリープ
- 初回アクセス時に起動に時間がかかる(15-30秒)
- 対策: フロントエンドでローディング表示を実装

  1. Vercel無料プランの制限
    • サーバーレス関数の実行時間制限(10秒)
    • 月間の使用制限
    • 対策: 計算処理は全てRender.com側で実行

更新版: UptimeRobotを使った無料プランのスリープ回避策

  1. 無料のUptimeRobotで解決可能

    • UptimeRobotの無料プランを利用して5分ごとに監視を設定
    • これによりRenderサービスが常にアクティブな状態を維持できる
    • スリープ状態になることがなくなり、初回アクセス時の遅延が解消
  2. 実装上の注意点

    • UptimeRobotはデフォルトでHEADリクエストを使用するため、以下のコードを追加する必要がある
    @app.head("/health")
    async def health_head():
        return None
        
    @app.get("/health")
    async def health_check():
        return {"status": "ok"}
    
    
  3. Vercel無料プランの制限

    • サーバーレス関数の実行時間制限(10秒)
    • 月間の使用制限
    • 対策: 計算処理は全てRender.com側で実行

まとめ

Vercel + Render.comの組み合わせは、Next.jsとPythonを連携させる無料デプロイ戦略です。それぞれのプラットフォームの強みを活かし、弱点を補完できます。

この構成だと、「NumPy計算機」だけでなく、機械学習モデルの推論APIや、データ分析ツールなど様々なPython+JavaScript連携アプリケーションに応用できる?と考えています。
無理ならどなたか教えていただきたいです。

最後に、私自身、最初はVercelだけでNext.js+Pythonを動かそうとして多くの壁にぶつかりました。アーキテクチャの分離という発想の転換が、結果的に最適解につながったと実感しています。


実装コードの完全版はGitHubで公開しています:
リポジトリへのリンク

お役に立てましたら、いいねやコメントをいただけると嬉しいです!

Discussion