見出し画像

RenderとVercelを使ってFastAPIで書かれたPythonプロジェクトをサービスとしてデプロイする(後編)

前後編に分かれてしまったRenderとVercelを使ってFastAPIで書かれたPythonプロジェクトをサービスとしてデプロイする(後編)です。
(すみません、備忘録です)

リポジトリは文末にサーバー側もフロントエンド側も公開しときます。

フロントエンド編

前回、Claudeにぶち込んで終わったフロントエンドですが、いちおうゼロから作っていきます。
NextJSフロントエンドのリポジトリを作ります。

FastAPIで作ったAPIサーバーに書き込むNext.JSのフロントエンド。
名前は"fast-todo"とします。
NextJSの命名規則でプロジェクト名に大文字は使えないので注意。

  • Nodeの .gitignore

  • Publicである必要はありません(Privateでよし)

  • readmeは不要です(作っちゃったら後で削除できます)

できあがったら、いきなりCodespacesにいっちゃいましょう。

下部のターミナルに node --version を打ってバージョンを見ます。
右クリックで貼り付けできます。ブラウザに許可してあげてください。

node --version
v20.15.1

Next.JSの新しいプロジェクトを「このディレクトリ」に作成します。

npx create-next-app@latest .

コマンドを実行すると、いくつかの質問が表示されます。左右キーを使って以下のように回答してください:

  • OK to proceed? (y) [Enter]

  • Would you like to use TypeScript? › No

  • Would you like to use ESLint? › Yes

  • Would you like to use Tailwind CSS? › Yes

  • Would you like to use src/ directory? › No

  • Would you like to use App Router? › No

  • Would you like to customize the default import alias? › No

続いて起動します

npm install
npm run dev

本来は localhost:3000で上がる開発環境ですが、port forwardingトンネルを掘って別タブでアクセスできるようになります。


できあがりです

生成されたプロジェクトの構造は以下のようになっています

fast-todo/
├── pages/
│   ├── _app.js
│   └── index.js
├── styles/
│   ├── globals.css
│   └── Home.module.css
├── public/
├── package.json
└── next.config.js

いったんここでコミット&アップデートしましょう。



pages/index.js を編集して、先程のToDoアプリのコードを実装します。

import { useState, useEffect } from 'react';
import { TodoList } from '../components/TodoList';
import { TodoForm } from '../components/TodoForm';

export default function Home() {
  const [todos, setTodos] = useState([]);

  useEffect(() => {
    fetchTodos();
  }, []);

  const fetchTodos = async () => {
    try {
      const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/todo`);
      const data = await response.json();
      setTodos(data);
    } catch (error) {
      console.error('Error fetching todos:', error);
    }
  };

  const addTodo = async (title) => {
    try {
      const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/todo`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ title }),
      });
      if (response.ok) {
        fetchTodos();
      }
    } catch (error) {
      console.error('Error adding todo:', error);
    }
  };

  const deleteTodo = async (id) => {
    try {
      const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/todo/${id}`, {
        method: 'DELETE',
      });
      if (response.ok) {
        fetchTodos();
      }
    } catch (error) {
      console.error('Error deleting todo:', error);
    }
  };

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-3xl font-bold mb-4">ToDo App</h1>
      <TodoForm addTodo={addTodo} />
      <TodoList todos={todos} deleteTodo={deleteTodo} />
    </div>
  );
}

また、components ディレクトリを作成し、TodoForm.js と TodoList.js を追加します。

/components/TodoForm.js

import { useState } from 'react';

export function TodoForm({ addTodo }) {
  const [title, setTitle] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!title.trim()) return;
    addTodo(title);
    setTitle('');
  };

  return (
    <form onSubmit={handleSubmit} className="mb-4">
      <input
        type="text"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="新しいタスクを入力"
        className="border p-2 mr-2"
      />
      <button type="submit" className="bg-blue-500 text-white p-2 rounded">
        追加
      </button>
    </form>
  );
}

/components/TodoList.js

export function TodoList({ todos, deleteTodo }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id} className="flex items-center mb-2">
          <span className="flex-grow">{todo.title}</span>
          <button
            onClick={() => deleteTodo(todo.id)}
            className="bg-red-500 text-white p-1 rounded"
          >
            削除
          </button>
        </li>
      ))}
    </ul>
  );
}

ここまでできたら再度、npm run devします(止めていた場合)。
なんとなく動作してます(サーバAPIがないので保存できません)

再度コミット&アップデートしましょう。


フロントエンドはvercelへデプロイ

フロントエンドはRenderではなくVercelにデプロイします。
Next.JSはVercelが作ったものなので!


NEXT_PUBLIC_API_URL

環境変数 NEXT_PUBLIC_API_URL を設定することを忘れないでください。開発環境では .env.local ファイルに NEXT_PUBLIC_API_URL=http://localhost:8000 を追加し、本番環境では適切なURLに変更してください。

NEXT_PUBLIC_API_URL= ???.onrender.com (スラッシュ不要!!)


デプロイ成功です。
アクセスしてみましょう。たぶん何も起きません。

F12(検証/開発者ツール)コンソールで見るとエラーだらけです。
このエラーは、Cross-Origin Resource Sharing (CORS) ポリシーによってリクエストがブロックされていることを示しています。が設定されていません。

再びサーバー側のコードを修正。

CORS設定のコードは、FastAPIアプリケーション(Render.comにあるバックエンド)のメインファイルに追加する必要があります。通常、このファイルは app.py や main.py といった名前で、Render.comにデプロイしているFastAPIプロジェクトのルートディレクトリにあります。

app.pyの冒頭に追記

import os
from fastapi import FastAPI, Depends
from fastapi.middleware.cors import CORSMiddleware
import uvicorn

from schemas import PostTodo
from models import TodoModel
from settings import SessionLocal

from sqlalchemy.orm import Session


app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    allowed_origins = os.getenv("ALLOWED_ORIGINS", "http://localhost:3000").split(",")  # 環境変数から許可するオリジンを取得
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 以下は従来のコードと同じ

FastAPIの標準ライブラリにはCORSミドルウェアが含まれているため、通常は追加のパッケージのインストールや requirements.txt の修正は不要ですが、環境変数を使用したCORS設定を採用する場合、python-dotenv パッケージを追加すると便利です。この場合、requirements.txt に以下を追加します。


fastapi
uvicorn
SQLAlchemy
python-dotenv

また、Render.comの環境変数設定で ALLOWED_ORIGINS を設定することを忘れないでください。

ALLOWED_ORIGINS=https://????.vercel.app,https://???-projects.vercel.app,http://localhost:3000

Render.comのダッシュボードで設定していきます。

完成!

URLを丸さらししているのと、RenderのサーバーはFree版なので止まると思います。みんなが遊び続けてくれれば生き続けるかもしれませんが…。

リポジトリはこちらです。スターよろしくお願いします!

RenderとVercelを使ってFastAPIで書かれたPythonプロジェクトをサービスとしてデプロイする(前編)


この記事が気に入ったらサポートをしてみませんか?