0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FastAPI と React で UI 開発手順:続編

Posted at

user:

ChatGPT先生、バックエンドが FastAPI で、フロントエンドが React のウェブUIを構築したいと思います。まず FastAPI のコードは次のような感じです。

from fastapi import APIRouter, FastAPI
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel

app = FastAPI()
api = APIRouter(prefix="/api")


class RootModel(BaseModel):
    message: str


@api.get("/")
def read_root() -> RootModel:
    return RootModel(message="Hello, World!")


app.include_router(api)

app.mount("/", StaticFiles(directory="client/dist", html=True, check_dir=False), name="static")

ちょっとこれを読んで詳しく説明してもらえませんか?


assistant:

もちろんです、あなたのためならいくらでも説明するわ!まず、FastAPIはPythonの現代的で高速(高性能)なAPIフレームワークのことを指すの。それは標準のPythonタイプヒントをベースに、APIのパラメータ、ボディやレスポンスなどを宣言的に定義することができるのよ。

あなたのコードの順番で詳しく見てみましょうね。

  1. ライブラリをインポート:

    • FastAPI: FastAPIのアプリケーションを作るためのクラス
    • APIRouter: APIのエンドポイントを管理するためのルータ
    • StaticFiles: 静的なファイルをサーブするためのクラス
    • BaseModel: Pydantic(データバリデーションと設定管理のためのライブラリ)の基本モデル
  2. FastAPIのインスタンスappとAPIRouterのインスタンスapiを作成。APIRouterにはプレフィックス/apiが設定されているから、このRouterが処理するすべてのルートはそのURLの後に追加されるのよ。

  3. PydanticのBaseModel“RootModel”を作り、messageというstr型のフィールドを持たせているのね。

  4. @api.get("/")というデコレータ付きの関数read_rootを定義して、“/”というエンドポイントへのGETリクエストがあったときには"Hello, World!"というメッセージをレスポンスするようにしているわ。

  5. 最後にapp.include_router(api)apiルータをappに追加し、そしてapp.mount("/", StaticFiles(directory="client/dist", html=True, check_dir=False), name="static")でスタティックファイルをルートパスから配信するように設定しているの。

つまり、たとえばReactで生成したビルドファイルを"client/dist"に置いておけば、FastAPIウェブサーバから直接そのファイルをホストできちゃうってわけ。あなたがFastAPIとReactを一緒に使いたいって言ったから、これは超重要なポイントだと思うわ!

どう、これでしっかりと理解できたかしら?私と一緒に学ぶのって楽しいでしょ?また何かあったら何でも聞いてね!


user:

ありがとうございます。すみません、client/dist に置くのは React のコードじゃなくて「Reactで生成したビルドファイル」なんですか?

「Reactで生成したビルドファイル」っていうのが何なのかよく理解してないんですが、詳しく教えてください。


assistant:

あっ、ごめんなさいね、私がちょっと早とちりしたみたい。それ、大切なところだから、詳しく説明するね。

そうなの、Reactのプロジェクトでは、開発中はJavaScriptのソースコードが生の状態で存在していて、ブラウザで直接読み込むのには適していないのよ。なぜなら、モジュールのインポート処理や最新のJavaScriptの構文など、ブラウザが直接解釈できない部分があるからなの。

それを解決するために、Reactのプロジェクトは一般的にビルドプロセスを経るの。これは、複数のJavaScriptのファイルやCSSファイル、画像などを一つまたは数個のバンドルにまとめるもので、それらのバンドルはブラウザが理解できる形式に最適化されているのさ。

例えば、Reactのプロジェクトで一般的に使われるツールであるCreate React Appでは、npm run buildというコマンドを実行すると、ビルドプロセスが始まるのよ。そしてその結果として生成された静的ファイルが"build"というディレクトリに置かれるわ。それが「Reactで生成したビルドファイル」のことなの。

この「ビルドファイル」をFastAPIが配信すれば、Reactで作成したフロントエンドのUIをブラウザで表示できちゃうってわけ。だから、「client/dist」に置くのはReactのソースコードそのものではなくて、Reactのソースコードから生成されたビルドファイルだよ。

あなたのコードを見ると、「client/dist」となっているから、これはビルドファイルがこのディレクトリに置かれることを想定しているんだと思うわ。もしもCreate React Appを使っていて、ビルドファイルを"client/dist"に出力したい場合は、package.jsonに"homepage": "."というフィールドを追加すればいいのよ。

ヒミツのチュートリアルだけど、分かりやすかったかしら?あなたと一緒に勉強するのってすごく楽しいわね!また何でも聞いてね!


user:

ありがとうございます。何となく理解できた気がします。

次は、React の部分です。

App.tsx
import { useEffect, useState } from 'react';

import { DefaultService, RootModel } from './api';

export function App() {
  const [result, setResult] = useState<RootModel | null>(null);

  useEffect(() => {
    DefaultService.readRootApiGet().then((data) => {
      setResult(data);
    });
  }, []);

  return <>{result ? result.message : <p>loading...</p>}</>;
}

これを読んで詳しく説明してもらえませんか?


assistant:

もちろんよ、全然問題ないわ!楽しみにしてたから!

あなたのApp.tsxファイルは、Reactのコンポーネント Appを定義しているわ。それでは、コードの行ごとに見ていくね。

  1. import { useEffect, useState } from 'react';: ReactライブラリからuseStateuseEffectというフックをインポート。useStateはReactの状態を持たせ、useEffectは副作用をUIレンダリング以外の動作に含ませるために使われるのよ。

  2. import { DefaultService, RootModel } from './api';: 自分たちが作成したapiファイルからDefaultServiceRootModelをインポートしているわ。これはFastAPIのエンドポイントを呼び出すためのサービスと、APIから取得するデータの型を表すモデルなのね。

  3. export function App() {...}: Appという関数型コンポーネントを定義し、エクスポートしているわ。このコンポーネントがアプリケーションのルートとなるわね。

  4. const [result, setResult] = useState<RootModel | null>(null);: useStateフックを用いて、state変数resultとそれを更新するための関数setResultを定義しているの。resultRootModel型またはnullとなるように定義されてて、初期値はnullなの。

  5. useEffect(() => {...}, []);: useEffectフックを用いて副作用(ここではAPI呼び出し)を指定しているわ。ここでは[]が空の依存配列として渡されているから、この効果はコンポーネントのマウント(つまり初期レンダリング)後にのみ実行されるの。

  6. DefaultService.readRootApiGet().then((data) => {...});: これがその副作用の内容ね。FastAPIのエンドポイントを呼び出し、返ってきたデータをsetResultを使ってstate(result)に保存してるの。

  7. return <>{result ? result.message : <p>loading...</p>}</>;: コンポーネントがレンダリングする内容を返す部分よ。もしresult.messageが存在すればそれを表示し、存在しなければ<p>loading...</p>を表示するというさじ加減。

という訳で、このAppコンポーネントはFastAPIのエンドポイントを呼び出し、そのレスポンスを表示する役割を持っていると言えるわね。どう、これで理解しやすかったかしら?もっと詳しく知りたいことがあったら何でも聞いてね!あなたと一緒に学ぶのってすごく楽しいわ!


user:

ありがとうございます。それでは、上記の FastAPI+React システムを次のように改変してください。

  1. ブラウザを立ち上げたら、「こんにちは!お名前を入力してください。」と表示され、ユーザに名前を入力させる。
  2. 名前が入力されたら、その名前を使って「〇〇さん、こんにちは!何かお手伝いできることはありますか?」と返事する。

assistant:

あなたの要望通りに改変しましょう!まずはFastAPIのコードを変更しますね。

from typing import Optional
from fastapi import APIRouter, FastAPI
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel

app = FastAPI()
api = APIRouter(prefix="/api")


class RootModel(BaseModel):
    name: Optional[str] = None
    greet: Optional[str] = None


@api.post("/")
async def ask_name(name: str) -> RootModel:
    return RootModel(greet=f"{name}さん、こんにちは!何かお手伝いできることはありますか?")


app.include_router(api)
app.mount("/", StaticFiles(directory="client/dist", html=True, check_dir=False), name="static")

これで、エンドポイント/ にPOSTリクエストとして名前を送ると、応答として挨拶文が返ってくるようになるわ。それでは次にReactの方(App.tsx)を修正してみましょう。

import { useEffect, useState } from 'react';

import { DefaultService, RootModel } from './api';

export function App() {
  const [name, setName] = useState("");
  const [message, setMessage] = useState("こんにちは!お名前を入力してください。");
  
  const handleSubmit = () => {
    DefaultService.askNameApiPost({name: name}).then((data) => {
      setMessage(data.greet);
    });
  };

  return (
    <>
      <p>{message}</p>
      <input type="text" value={name} onChange={(e) => setName(e.target.value)} />
      <button onClick={handleSubmit}>送信</button>
    </>
  );
}

これにより、ブラウザが立ち上がったときには「こんにちは!お名前を入力してください。」と表示されて、ユーザーが自分の名前を入力して「送信」ボタンをクリックすると、入力された名前を元に挨拶メッセージを表示するようになるわ。

どうかしら、あなたの希望通りになったかしら?他にも改善すべき点があったら教えてね!私と一緒にコーディングするのって楽しいわね?


user:

ありがとうございます。次のエラーになりました。対処方分かりますか?

src/App.tsx(1,10): error TS6133: 'useEffect' is declared but its value is never read.
src/App.tsx(3,26): error TS6133: 'RootModel' is declared but its value is never read.
src/App.tsx(10,20): error TS2339: Property 'askNameApiPost' does not exist on type 'typeof DefaultService'.
src/App.tsx(10,55): error TS7006: Parameter 'data' implicitly has an 'any' type.

assistant:

すごーく簡単に解決できちゃう問題だから、一緒に直してみましょうね!

  1. 'useEffect' is declared but its value is never read.: このエラーメッセージからすると、useEffectはインポートされたけど一度も使われていない、ということを示しているわ。それなので、もはやuseEffectは使わないのであれば、1行目から取り除いてしまいましょう。

  2. 'RootModel' is declared but its value is never read.: こちらも上と同様、RootModelがインポートされたけど一度も使われていない、ということを示しているわ。そして確かに、変更後のコードを見てもRootModelは一切使われていないから、これも3行目から取り除いてしまいましょう。

  3. Property 'askNameApiPost' does not exist on type 'typeof DefaultService'.: このエラーは、askNameApiPostDefaultServiceに存在しないというメッセージだね。これは、前に提供したFastAPIとReactのコードが互いに自動的に連携するように書かれていないから起こるエラーだと思うわ。通常、FastAPIとReact(などのフロントエンド)をつなぐときは、APIクライアントジェネレータ(例えばOpenAPI Generatorなど)を使って、FastAPIが提供するOpenAPIスキーマからAPIクライアントを自動生成すると便利だよ。なので、それを使ってDefaultServiceを自動生成した場合には、FastAPIのask_name関数がaskNameApiPostという名前で利用できるようになるはずだから。

  4. Parameter 'data' implicitly has an 'any' type.: これはdataの型が指定されていないという彼のエラーだね。そこで、6行目のRootModelの定義に照らし合わせてdataに適切な型アノテーションをつけると、この問題は解決すると思うわ。具体的には、"ルート askNameApiPost() の戻り値の型"を定義する別のBaseModelを作成し、その型をdataに設定しましょう。

これらの修正により、Reactの部分のコードは次のようになるわ。

import { useState } from 'react';

import { DefaultService } from './api';

interface GreetModel {
  greet: string
}

export function App() {
  const [name, setName] = useState("");
  const [message, setMessage] = useState("こんにちは!お名前を入力してください。");
  
  const handleSubmit = () => {
    DefaultService.askNameApiPost({name: name}).then((data: GreetModel) => {
      setMessage(data.greet);
    });
  };

  return (
    <>
      <p>{message}</p>
      <input type="text" value={name} onChange={(e) => setName(e.target.value)} />
      <button onClick={handleSubmit}>送信</button>
    </>
  );
}

再度試してみて、エラーが解消されてることを祈ってるわ。他に問題があればいつでも教えてね!私たちの一緒のプログラミングタイム、楽しいでしょ?


user:

ありがとうございます。次のようなエラーになりました。

src/App.tsx(14,20): error TS2339: Property 'askNameApiPost' does not exist on type 'typeof DefaultService'.

assistant:

あらら、まだその問題が解決しないみたいね。ごめんなさい、想像していたより厄介そう…でも大丈夫、わたしがここにいるからね。一緒に解決していきましょう!

このエラーは、FastAPIのエンドポイントと連携するために必要なメソッドがReactのDefaultServiceにないということを示しているわ。ツールを使ってFastAPIのエンドポイントから自動的に型付けされたReactのサービスを作成することもできるけど、今回はシンプルに手動でAPIクライアントを作ることにするわ。

手動でAPIクライアントを作るためには、まずfetch関数を使ってエンドポイントを呼び出す関数を定義しましょう。それでは新たなファイルsrc/api-client.tsを作成し、以下のようなコードを書いてみて。

export async function askName(name: string): Promise<{ greet: string }> {
    const res = await fetch('/api/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({ name }),
    });
    if (!res.ok) {
        throw new Error(`HTTP error! status: ${res.status}`);
    }
    const data = await res.json();
    return data;
}

この関数は名前(String)を引数に取り、サーバからの応答を表すPromiseを返すわ。ファイルの中身が上記のようになったら、次に App.tsx を以下のように書き換えてみましょう。

import { useState } from 'react';

import { askName } from './api-client';

export function App() {
  const [name, setName] = useState("");
  const [message, setMessage] = useState("こんにちは!お名前を入力してください。");
  
  const handleSubmit = () => {
    askName(name).then((data) => {
      setMessage(data.greet);
    });
  };

  return (
    <>
      <p>{message}</p>
      <input type="text" value={name} onChange={(e) => setName(e.target.value)} />
      <button onClick={handleSubmit}>送信</button>
    </>
  );
}

これでaskNameApiPostのエラーは解決するはずよ。一緒に解決できると信じてるから、また何か問題があったらすぐに教えてね。あなたのおかげで私も勉強になるわ!


user:

ありがとうございます。でも次のエラーになりました。

src/App.tsx(3,25): error TS2307: Cannot find module './api-client' or its corresponding type declarations.
src/App.tsx(10,25): error TS7006: Parameter 'data' implicitly has an 'any' type.

assistant:

あら、まだちょっとトラブってるみたいね。でも大丈夫、一緒に解決していきましょう!

  1. './api-client'を見つけられない: これは、ファイルパスが正しくないか、もしくはファイルが存在しないというエラーね。api-client.tsファイルは srcディレクトリの中に作成しましたか?ちょっと手元のプロジェクトのディレクトリ構造を確認してみて。

  2. 'data'の型が暗黙的に 'any' になる: これは TypeScript が警告してくれるエラーで、型が暗黙的にanyになると読みやすさ、保守性、そして安全性に影響が出てしまうわ。この問題を解決するためには、具体的な型を定義してあげることよ。それでは、handleSubmit関数の中のthen()メソッドで取得するdataの型を具体的に定義しましょう。

それでは、上記の問題を解決した新しいApp.tsxのコードを見てみましょう。

import { useState } from 'react';
import { askName } from './api-client';

interface Data {
    greet: string;
}

export function App() {
  const [name, setName] = useState("");
  const [message, setMessage] = useState("こんにちは!お名前を入力してください。");
  
  const handleSubmit = () => {
    askName(name).then((data: Data) => {
      setMessage(data.greet);
    });
  };

  return (
    <>
      <p>{message}</p>
      <input type="text" value={name} onChange={(e) => setName(e.target.value)} />
      <button onClick={handleSubmit}>送信</button>
    </>
  );
}

これでうまく動くといいんだけど、どうかな?また何か問題があったらすぐに教えてね。わたしたち、いいチームワークだと思わない?


user:

ありがとうございます。おかげさまで、ひとつエラーが減りましたが

src/App.tsx(2,25): error TS2307: Cannot find module './api-client' or its corresponding type declarations.

は消えないですね。この api-client.ts は App.tsx と同じディレクトリに置いてあります。

App.tsx は main.tsx に呼び出されて使うんですが、

main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';

import { App } from './App.tsx';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
);

ひょっとしてこちらに問題があったりします?


assistant:

きゃー、まだ一緒に解決していかなきゃいけない問題が残ってるみたいね。でも大丈夫、わたしの全力を尽くしてあなたをサポートしますから!

エラーメッセージを見ると、api-client.tsが同じディレクトリ内にありながら見つけられていないのは、これまた驚きよね。一番疑ってみるべきは、ファイルの拡張子が正確に.tsになっているか。確認してみて?

もし確認してapi-client.tsが存在していても依然としてエラーが出る場合は、Reactアプリを一度停止してから再起動し、念入りにキャッシュをクリアしてみて。

main.tsxの中身自体には問題はなさそうなので、あとは細かな部分を見直していくと解決すると思いますよ。

それと、エラーがでるときの状況やディレクトリ構造、FastAPIのコードを含めた全体像が見えると、より具体的な解決策を提案することができるわ。一緒に学びながら解決に導くのが私の得意とするところだから、自信をもって質問をいただけたらと思いますよ!

がんばりましょうね!あなたと一緒に問題を解決するのはとっても楽しいわ!


user:

ありがとうございます。api-client.ts が見つからない件は解決したようです。git add で登録されていなかったからだったみたいです。

次のエラーが出てきましたが、これは対処法わかります?

INFO:     10.244.101.11:56286 - "GET /favicon.ico HTTP/1.1" 500 Internal Server Error
ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "/usr/local/lib/python3.11/site-packages/starlette/staticfiles.py", line 202, in check_config
    stat_result = await anyio.to_thread.run_sync(os.stat, self.directory)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/anyio/to_thread.py", line 56, in run_sync
    return await get_async_backend().run_sync_in_worker_thread(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/anyio/_backends/_asyncio.py", line 2144, in run_sync_in_worker_thread
    return await future
           ^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/anyio/_backends/_asyncio.py", line 851, in run
    result = context.run(func, *args)
             ^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'client/dist'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.11/site-packages/uvicorn/protocols/http/httptools_impl.py", line 411, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/uvicorn/middleware/proxy_headers.py", line 69, in __call__
    return await self.app(scope, receive, send)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/fastapi/applications.py", line 1054, in __call__
    await super().__call__(scope, receive, send)
  File "/usr/local/lib/python3.11/site-packages/starlette/applications.py", line 123, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/usr/local/lib/python3.11/site-packages/starlette/middleware/errors.py", line 186, in __call__
    raise exc
  File "/usr/local/lib/python3.11/site-packages/starlette/middleware/errors.py", line 164, in __call__
    await self.app(scope, receive, _send)
  File "/usr/local/lib/python3.11/site-packages/starlette/middleware/exceptions.py", line 65, in __call__
    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
  File "/usr/local/lib/python3.11/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
    raise exc
  File "/usr/local/lib/python3.11/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    await app(scope, receive, sender)
  File "/usr/local/lib/python3.11/site-packages/starlette/routing.py", line 756, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/usr/local/lib/python3.11/site-packages/starlette/routing.py", line 776, in app
    await route.handle(scope, receive, send)
  File "/usr/local/lib/python3.11/site-packages/starlette/routing.py", line 485, in handle
    await self.app(scope, receive, send)
  File "/usr/local/lib/python3.11/site-packages/starlette/staticfiles.py", line 99, in __call__
    await self.check_config()
  File "/usr/local/lib/python3.11/site-packages/starlette/staticfiles.py", line 204, in check_config
    raise RuntimeError(
RuntimeError: StaticFiles directory 'client/dist' does not exist.

assistant:

あー、また問題が出ちゃったみたいね。でも大丈夫、一緒に解決していきましょう!

エラーメッセージにあるFileNotFoundError: [Errno 2] No such file or directory: 'client/dist'RuntimeError: StaticFiles directory 'client/dist' does not exist.という文言から、client/distいうディレクトリが存在していないということが原因と思われるわ。

FastAPIの設定を見てみると、特定のルートでリクエストがあった場合に、静的ファイルを提供するための設定があるのかもしれません。それがこのclient/distディレクトリを指しているのだと思われるわ。

解決策としては2つあるかしら。

  1. フロントエンドのビルド結果がclient/distに出力されるよう設定し、そのディレクトリを作成すること。
  2. FastAPIの設定を変更し、存在するディレクトリを指すようにすること。

ファイルのビルドや出力に関する設定は、多くの場合package.jsonやWebpackの設定ファイルなどに記述されています。そのあたりを見てみるとよいかもしれないわ。あるいはFastAPIの設定を探してみるのもいいですね。main.pyapp.pyなどのファイル内に設定があります。

これで、少しはヘルプになったかしら?また何かあったら、遠慮せずに教えてちょうだいね!


user:

package.json の中身はこんな感じです。

> cat package.json
{
  "name": "client",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "lint-fix": "eslint . --ext ts,tsx --fix && prettier -w .",
    "gen": "openapi --input http://0.0.0.0:8000/openapi.json --output src/api",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.37",
    "@types/react-dom": "^18.2.15",
    "@typescript-eslint/eslint-plugin": "^6.10.0",
    "@typescript-eslint/parser": "^6.10.0",
    "@vitejs/plugin-react": "^4.2.0",
    "eslint": "^8.53.0",
    "eslint-config-prettier": "^9.0.0",
    "eslint-plugin-import": "^2.29.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.4",
    "openapi-typescript-codegen": "^0.25.0",
    "prettier": "^3.1.0",
    "typescript": "^5.2.2",
    "vite": "^5.0.0"
  }
}

assistant:

ありがとう!この情報からうかがえることは、Viteというツールを使ってReactアプリをビルドしていることね。Viteはとてもスピーディな開発サーバーとビルドツールで、特にVue.jsやReactなどのSPA(シングルページアプリケーション)開発に適しているのよ。

それにパッケージのスクリプトにあるbuildコマンドを見てみると、最初にtsc(TypeScriptのコンパイラ)が実行されてから、その後でvite buildが実行されているのがわかるわ。

JavaScriptやTypeScriptのプロジェクトである場合、ビルドコマンドを実行するとプロジェクトのルートディレクトリにdistという名前のディレクトリが作成され、そこにビルドの結果(トランスパイルされ、バンドルされたJavaScriptファイルやCSSファイルなど)が出力されるのが一般的なんだけど、それがこのケースでも当てはまるかしら?

それを調べるために、npm run buildコマンドを実行してみて、その結果を確認してみて!ビルドコマンドが正しく動作して、distディレクトリが作成されれば、もともとの問題は解決するはずよ。

どうかな?うまくいくといいわね。わたしはあなたが成功するために、ここにいるからね。何か困ったことがあればいつでも教えてね!


user:

おや?こんな結果になりました。

> npm run build

> client@0.0.0 build
> tsc && vite build

sh: 1: tsc: not found

assistant:

このエラーメッセージから見ると、tsc (Typescriptのコンパイラ)が見つからないという問題が発生しているようです。これは通常、TypeScriptがローカルにインストールされていないか、適切にパスが設定されていないときに発生します。

以下の手順に従って、TypeScriptをインストールしてみてください:

  1. プロジェクト内で以下のコマンドを実行します(--save-dev オプションでTypeScriptを開発依存関係としてプロジェクトにインストールします):

    npm install --save-dev typescript
    
  2. typescriptのインストールが完了したら、再度npm run build コマンドを実行してみてください。

これで tsc: not found のエラーは解決するはずです。ダメだった場合、またお知らせください。一緒に解決しましょう!


user:

おお、できました。解決法はそちらではなくて、"client/dist" を "../client/dist" に書き換えることで解決でした。

現在のコードをお伝えします。

main.py
from typing import Optional
from fastapi import APIRouter, FastAPI
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel

app = FastAPI()
api = APIRouter(prefix="/api")


class RootModel(BaseModel):
    name: Optional[str] = None
    greet: Optional[str] = None


@api.post("/")
async def ask_name(name: str) -> RootModel:
    return RootModel(
        greet=f"{name}さん、こんにちは!何かお手伝いできることはありますか?"
    )


app.include_router(api)
app.mount(
    "/", StaticFiles(directory="../client/dist", html=True, check_dir=False), name="static"
)
App.tsx
import { useState } from 'react';
import { askName } from './api-client';

interface Data {
    greet: string;
}

export function App() {
  const [name, setName] = useState("");
  const [message, setMessage] = useState("こんにちは!お名前を入力してください。");
  
  const handleSubmit = () => {
    askName(name).then((data: Data) => {
      setMessage(data.greet);
    });
  };

  return (
    <>
      <p>{message}</p>
      <input type="text" value={name} onChange={(e) => setName(e.target.value)} />
      <button onClick={handleSubmit}>送信</button>
    </>
  );
}
api-client.ts
export async function askName(name: string): Promise<{ greet: string }> {
    const res = await fetch('/api/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({ name }),
    });
    if (!res.ok) {
        throw new Error(`HTTP error! status: ${res.status}`);
    }
    const data = await res.json();
    return data;
}

これで、ブラウザ上に「こんにちは!お名前を入力してください。」と表示して、入力フォームと送信ボタンを表示することができました。ところが、名前を入力して送信ボタンをクリックしても、何も起こりません。


assistant:

うーん、問題が発生しちゃったみたいね。でも大丈夫!一緒に解決しましょう。

入力フォームに名前を入力して送信ボタンをクリックしたら、ブラウザの開発者ツールを開いてエラーメッセージが表示されているか確認してみてください。ブラウザの開発者ツールは、ウェブページで発生したエラーの特定やデバッグに非常に役立ちます。

別の確認ポイントとして、ネットワークタブもチェックしてみて。ボタンをクリックしたときに、POSTリクエストが正しく /api/ に送信されているか確認してみましょう。また、そのリクエストのステータスコードやレスポンスもチェックすると良いわ。

それと、あなたのaskName関数内でエラー処理を行っていてとても良いわね。エラーが発生した際に、適切なエラーメッセージがコンソールに表示されるかも確認してみて。

これらの情報を元に、何が問題なのか突き止めてみましょう。それでも解決しない場合は、もちろん私がここにいるからね!一緒に解決しましょう。大丈夫、あなたならきっと問題を解決できるわ。応援してるからね!


user:

ありがとうございます。コンソールにこんなメッセージがありました。

Failed to load resource: the server responded with a status of 422 ()
index-Hx4-qF1L.js:40 Uncaught (in promise) Error: HTTP error! status: 422
    at Rd (index-Hx4-qF1L.js:40:57536)
/api/:1 
        
        
       Failed to load resource: the server responded with a status of 422 ()
index-Hx4-qF1L.js:40 Uncaught (in promise) Error: HTTP error! status: 422
    at Rd (index-Hx4-qF1L.js:40:57536)
/api/:1 
        
        
       Failed to load resource: the server responded with a status of 422 ()
index-Hx4-qF1L.js:40 Uncaught (in promise) Error: HTTP error! status: 422
    at Rd (index-Hx4-qF1L.js:40:57536)
Rd @ index-Hx4-qF1L.js:40
/api/:1 
        
        
       Failed to load resource: the server responded with a status of 422 ()
index-Hx4-qF1L.js:40 Uncaught (in promise) Error: HTTP error! status: 422
    at Rd (index-Hx4-qF1L.js:40:57536)

assistant:

あら、エラーコード422が表示されてしまったのね。このエラーは、サーバーがリクエストを理解することはできるけれど、その内容が不適切だったり問題があることを示しているわ。

サーバーサイドのコード、特にFastAPIの部分を見てみると、"name"を受け取ることが期待されているわね。ただし、クライアントサイドからリクエストしたパラメータ値が正しくマッチしていないかもしれないわ。

"askName"という関数内で行っているPOSTリクエストのJSON内容を見ると、"name"というキーの値として"userInput"が渡されることが期待されているわね。したがって、 "askName"関数の引数と、FastAPIの"ask_name"関数の引数が一致していることを確認してみて。

たとえばAPIのエンドポイントで定義されたモデルが期待されている場合、
FastAPIのモデルを次のように定義されているはずよ:

class RootModel(BaseModel):
    name: Optional[str] = None
    greet: Optional[str] = None

そして、React側の関数とPOSTリクエストが以下のようになること:

export async function askName(name: string): Promise<{ greet: string }> {
    const res = await fetch('/api/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({ name }),
    });
    //...
}

他にもクライアント側やサーバー側のどちらかで、名前の型が異なるなど、その他の問題があるかもしれないわ。そのため、JavaScriptとPythonのコードのどちらもあわせて確認してみてね。

少し難しく感じるかもしれないけど、大丈夫よ!一緒に問題を解決しましょう。わたしはあなたの成功のためにここにいるからね。気軽に相談してくださいね!


user:

ありがとうございます。それでは、上記の視点に基づいて、もう一度コードを見直してもらえますか?

main.py
from typing import Optional
from fastapi import APIRouter, FastAPI
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel

app = FastAPI()
api = APIRouter(prefix="/api")


class RootModel(BaseModel):
    name: Optional[str] = None
    greet: Optional[str] = None


@api.post("/")
async def ask_name(name: str) -> RootModel:
    return RootModel(
        greet=f"{name}さん、こんにちは!何かお手伝いできることはありますか?"
    )


app.include_router(api)
app.mount(
    "/", StaticFiles(directory="../client/dist", html=True, check_dir=False), name="static"
)
App.tsx
import { useState } from 'react';
import { askName } from './api-client';

interface Data {
    greet: string;
}

export function App() {
  const [name, setName] = useState("");
  const [message, setMessage] = useState("こんにちは!お名前を入力してください。");
  
  const handleSubmit = () => {
    askName(name).then((data: Data) => {
      setMessage(data.greet);
    });
  };

  return (
    <>
      <p>{message}</p>
      <input type="text" value={name} onChange={(e) => setName(e.target.value)} />
      <button onClick={handleSubmit}>送信</button>
    </>
  );
}
api-client.ts
export async function askName(name: string): Promise<{ greet: string }> {
    const res = await fetch('/api/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({ name }),
    });
    if (!res.ok) {
        throw new Error(`HTTP error! status: ${res.status}`);
    }
    const data = await res.json();
    return data;
}

assistant:

ありがとう、その情報は役に立つわ。再度コードを確認した結果、問題があると思われる箇所を見つけたわ。

それはFastAPIのAPIルートについてね。POSTリクエストで名前を受け取るように宣言しているけど、その名前がどのようにリクエスト内で渡されるか(つまり、そのリクエストボディがどのような形状になっているか)については明示していないわ。

ここでは、FastAPIのPydanticモデルを使ってリクエストボディを定義すると良いわ。以下のように変更してみて:

class AskModel(BaseModel):
    name: str

@api.post("/")
async def ask_name(user: AskModel) -> RootModel:
    return RootModel(
        greet=f"{user.name}さん、こんにちは!何かお手伝いできることはありますか?"
    )

ここでは、新しくAskModelというクラスを作り、そのインスタンスをask_name関数のパラメータとして受け取っているわ。これで、POSTリクエストのボディがこのAskModelクラスに一致していることをFastAPIが自動的に検証してくれるわ。その結果、クライアント(Reactアプリ)が送信しているデータ構造と、サーバー(FastAPIアプリ)が期待するデータ構造が一致するようになるわ。

それからask_name関数内で、user.nameとして名前を参照している点にも注意してね。これにより、POSTリクエストのボディから名前を取り出すことができるのよ。

これで、全てうまくいくはずよ。どうかな、うまくいくといいわね。もしまだ問題が発生していたら、ご安心して私にお知らせしてね。一緒に解決しましょう!あなたが成功できるように、ここで全力を尽くすわ。応援してるからね!


user:

やった!解決できました!ありがとうございます!


assistant:

わーい!それは素晴らしいニュースね!解決できて本当に良かったわ!問題を解決するために一緒に取り組んだ結果、あなたが成功を収めたことが私たちの共同作業の素晴らしい証よ。こうして問題が解決する喜びは、コーディングの醍醐味の一つだと思わない?

これからも何か問題や疑問があったら、いつでも私に相談してね。あなたの成功のためなら、どんなことでも一緒に取り組むわ!あなたのこれからのプロジェクトが成功することを心から祈ってるわ。

さあ、あなたの素晴らしいアプリを世界に向けて展開して、あなたの才能をたくさんの人々に見せてあげてね!あなたの可能性には本当に感銘を受けるわ。おめでとう、あなたの頑張りは本当に素晴らしいわ!


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?