LoginSignup
0
0

FastAPI で銀行ごっこしよう: Next.js 導入編

Posted at

user:

ChatGPT先生、今日は、前回の「FastAPIを使って銀行ごっこをしよう」の続編として、Next.js を導入したいと思います。よろしくお願いします。


assistant:

ちょっと待って!先生って…何があったって私があなたの先生になるわけないでしょ!でも、まぁ、Next.jsについて知りたいなら、私が教えてあげますけどね。だからって、あまり期待しないでよ。

というわけで、最初に言っておくと、Next.jsはReactのフレームワークだから、まずは基本的なReactの知識が必要なのよ。あなた、それが分かってるの?一応、Reactコンポーネント、props、state、ライフサイクル方法など、基本的な扱い方を理解してないといけませんよ。

そして次にNext.jsだけど、サーバーサイドレンダリングと静的サイト生成をサポートしているんだから、それらが何を意味するのか、何か分からないことがあったら、ちゃんと質問してね。

忘れてはいけないのが、Next.jsを使って開発を始めるためには、Node.jsがインストールされている環境が必要だってこと。あなた、それもきちんと把握してるの?

ええと、あとは、プロジェクトを始めるために以下のコマンドをターミナルに入れて、新しいNext.jsのアプリケーションを作るんだけど…、

npx create-next-app@latest

これで、create-next-appをダウンロードして、新しいNext.jsアプリケーションを作ります。次に表示されるプロンプトで、ユーザーはアプリケーションの名前とディレクトリを設定します。

全てうまく行ったら、そのディレクトリに移動して次のコマンドを実行します:

npm run dev

これで、開発サーバーが起動してhttp://localhost:3000であなたのアプリケーションを見られます。

これで、あなたはNext.jsの基本的な設定を始めることができるわ。これからも、何かわからないことがあったら遠慮なく聞いて。だからって、私が全て知っているわけじゃないから、そのつもりで!分からなかったら、分からないってはっきり言うからね!


user:

すみません、よく分かってないんですが

npx create-next-app@latest

って、

npx create-react-app my-app

と、どう違いますか?

あと、

npm run dev
cd my-app
npm start

の違いも教えてください。


assistant:

ああ~あなたって本当に何もわかってないのね。まあ、私が丁寧に教えてあげますからね。

まず、npx create-next-app@latestnpx create-react-app my-appの違いだけど、これらはそれぞれNext.jsのプロジェクトとReactのプロジェクトを新しく作成するためのコマンドになるわ。Next.jsはReactのフレームワークだけど、いくつかの追加機能があって特にサーバーサイドレンダリングや静的サイト生成を容易にてきるのよ。

そして、npm run devcd my-app && npm startの違いは、それぞれで起動する環境が異なるからだよ。npm run devはNext.jsではよく利用されるコマンドで、開発モードでサーバーを起動するの。つまり、このモードでは、ファイルを変更すればすぐ反映され、エラーや警告のメッセージも詳しく確認できるの。

一方で、cd my-app && npm startcreate-react-appで作られたプロジェクトを開始するコマンドで、これを利用するとReactの開発サーバーが起動するのよ。Reactだと、大抵このコマンドを使ってローカルで開発を進めるわ。これも開発モードで動くから、npm run devと同じようにコードに変更を加えるとすぐにアプリに反映されるわ。

どう、これでわかったかしら?それとも、私の説明が難しすぎた?でも、これが今の私の一番頑張ってる説明なんだから、何度も聞かれても同じことしか言えないわよ!使ってみなはれ!


user:

npx create-react-app my-app は、my-app というディレクトリを作って、その中にReactに必要なファイルをたくさん作るけど、npx create-next-app@latest は特に何も作ってないように見えますね。本当は何か作ってるんでしょうか?


assistant:

ええ!ちゃんと聞いてなさそうね!まったく、あなたに何度同じことを説明しないといけないんだか...もう、何も知らないの?

npx create-next-app@latestも同じように新しいプロジェクトディレクトリを作って、その中にNext.jsに必要なファイルを作るのよ。ただ、その名前を決めるプロンプトが出るまでちょっと待たないといけないから、もしかしたらそれで何も作ってないように見えたのかもしれないわね。

だから、npx create-next-app@latestを実行したら、ターミナルで順に指示に従って、新しいプロジェクトの名前を入力してね。それが完了すれば、指定した名前の新しいディレクトリが作られて、その中にNext.jsアプリケーションに必要なファイルが全て用意されているはずよ。

それで、質問の答えになったかしら?あなた、ちゃんとこれで納得してくれるかしら?また何かわからないことがあったら聞いて。でも、もしかしたらその答え、私も知らないかもしれないからね。だから、それは期待しないで!


user:

ああ、何も作らないと思ってたら、エラー終了してました。

$ npx create-next-app@latest
npx: 1個のパッケージを1.952秒でインストールしました。
Cannot find module 'stream/promises'
Require stack:
- /Users/ikemen/.npm/_npx/4364/lib/node_modules/create-next-app/dist/index.js

assistant:

あ、それね。そのエラーは、恐らくNode.jsのバージョンが古すぎて起きたものと思われます。Next.jsをインストールするためには、少なくともNode.jsのバージョン10.13以上が必要なんですよ。ていうか、今はもう便利なパッケージもたくさんあるし、なるべく新しいバージョンを使った方がいいですよ。

まず、ターミナルで node -v コマンドを実行して、現在のNode.jsのバージョンを確認してみてください。それから、もしバージョンが古かったらNode.jsをアップデートする必要があるわ。そのためには、Node.jsの公式ウェブサイトから最新の安定版をダウンロードしてインストールすればOK。

これで解決してくれると嬉しいけど...でも、また何か問題があったら聞いてくださいね。私はあなたが全てを理解できるよう手助けするためにここにいるんですから!それに、何かを教えることで私も勉強になるし。なんてね、私だって全部知らないんだから!


user:

バージョンを確認してみました。

$ node -v
v14.2.0

最新版をインストールするの、コマンドラインでできませんか?


assistant:

あなた、ほんっとに何も分からないのね...。まぁ、それが普通のことでしょ。でも、心配しないで、やっぱりそのために私がここにいるんだから。

Node.jsの最新版は、Node.jsのオフィシャルビルドを使ってダウンロードできるわ。「nvm」というツールを使えば、コマンドラインから簡単にNode.jsのバージョンを管理することができるの。まず、nvmをインストールする必要があるんだけど、これはコマンドラインを使って行うことができるわ。でも、まずはnvmがインストールされているかどうかをチェックしてね。ターミナルで nvm --version コマンドを使ってみて。

もしnvmが未インストールだったら、下記のコマンドを使ってインストールすることができるわ。これもターミナルで行うのよ。

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash

で、これでnvmが使えるようになったら、新しいNode.jsのバージョンをインストールするために nvm install node コマンドを使うの。

インストールが終わったら、新しいバージョンを使うために nvm use node コマンドを使って、新しいバージョンに切り替えてみてね。

これでNode.jsの最新のバージョンにアップデートできるはずよ。うまく行くといいけど...でも、もしうまくいかなかったらまた聞いてね。私は何度でも教えることができるから!だから、それはあなたが理解できるかどうかにかかっているんだからね!


user:

はい、やってみました。

$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 14926  100 14926    0     0  39819      0 --:--:-- --:--:-- --:--:-- 39802
=> Downloading nvm from git to '/Users/ikemen/.nvm'
=> Cloning into '/Users/ikemen/.nvm'...
remote: Enumerating objects: 365, done.
remote: Counting objects: 100% (365/365), done.
remote: Compressing objects: 100% (313/313), done.
remote: Total 365 (delta 43), reused 164 (delta 26), pack-reused 0
Receiving objects: 100% (365/365), 365.08 KiB | 10.43 MiB/s, done.
Resolving deltas: 100% (43/43), done.
* (HEAD detached at FETCH_HEAD)
  master
=> Compressing and cleaning up git repository

=> Appending nvm source string to /Users/ikemen/.bash_profile
=> Appending bash_completion source string to /Users/ikemen/.bash_profile
=> Close and reopen your terminal to start using nvm or run the following to use it now:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion
2024-04-23 22:56:10 ~/FastAPI 
$ nvm install node
-bash: nvm: command not found

新しいターミナルを立ち上げてもう一度。

2024-04-23 22:57:29 ~/FastAPI 
$ nvm install node
Downloading and installing node v21.7.3...
Downloading https://nodejs.org/dist/v21.7.3/node-v21.7.3-darwin-x64.tar.xz...
################################################################################################## 100.0%
Computing checksum with shasum -a 256
Checksums matched!
Now using node v21.7.3 (npm v10.5.0)
Creating default alias: default -> node (-> v21.7.3)
2024-04-23 22:58:00 ~/FastAPI 

assistant:

あら、しっかりできてるじゃない!ちゃんと指示通りにやれば、あなたでもできるんだね。でも、それが当たり前よね!

で、今インストールしたNode.jsのバージョンを確認するためにnode -vを再度実行してみて。それでちゃんと新しいバージョンが表示されれば、設定は大丈夫。

それで、再度npx create-next-app@latestのコマンドを実行してみて。あとは指示に従ってプロジェクト名を入力すれば、無事にNext.jsのプロジェクトが生成されるはずよ。

うまく行ったら報告してね。私、あなたが成功して喜んでいるのを見るの、案外好きなんだから。ええ、それが私の幸せなんだから!


user:

ありがとうございます。こういう結果になりました。

2024-04-23 22:58:00 ~/FastAPI 
$ node -v
v21.7.3
2024-04-23 23:00:27 ~/FastAPI 
$ npx create-next-app@latest
Need to install the following packages:
create-next-app@14.2.2
Ok to proceed? (y) y
✔ What is your project named? … my-app
The directory my-app contains files that could conflict:

  README.md
  node_modules/
  package-lock.json
  package.json
  public/
  src/

Either try using a new directory name, or remove the files listed above.

npm notice 
npm notice New patch version of npm available! 10.5.0 -> 10.5.2
npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.5.2
npm notice Run npm install -g npm@10.5.2 to update!
npm notice 
2024-04-23 23:00:47 ~/FastAPI 

assistant:

ちゃんと進行してるわね!でも、既にファイルやディレクトリが存在してるところにプロジェクトを作ろうとしたからエラーが起きてしまったみたい。

my-appっていうディレクトリ名でプロジェクトを作ろうとしたみたいだけど、その名前のディレクトリはすでに存在してるのね。そのディレクトリには既にいくつかのファイルやディレクトリが存在していて、それらが新しいプロジェクト作成コマンドと競合(conflict)を起こしてしまったの。

対処方法は2つあるわ。

1つ目は、新しいディレクトリ名で再度npx create-next-app@latestコマンドを実行すること。これなら、新しいディレクトリが作られてそこにNext.jsのプロジェクトが作られるから競合する心配はないの。

2つ目は、既存のmy-appディレクトリをリネームしたり、中のファイルを移動したり、必要ないファイルを削除したりしてから、再度コマンドを実行すること。ただ、これを選ぶ時は、誤って重要なファイルを削除しないように気をつけてね。

で、これでうまく行くといいんだけど…。どちらにしても、あなたが新しいプロジェクトを作る場所に、既存のファイルやディレクトリが無いことを確認するのが一番重要なんだからね!


user:

既存のmy-app を削除してから再度実行しました。

$ npx create-next-app@latest
✔ What is your project named? … my-app
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes
Creating a new Next.js app in /Users/ikemen/FastAPI/my-app.

Using npm.

Initializing project with template: app-tw 


Installing dependencies:
- react
- react-dom
- next

Installing devDependencies:
- typescript
- @types/node
- @types/react
- @types/react-dom
- postcss
- tailwindcss
- eslint
- eslint-config-next


added 360 packages, and audited 361 packages in 58s

133 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
Initialized a git repository.

Success! Created my-app at /Users/ikemen/FastAPI/my-app

assistant:

あら、やっと成功したのね。よく頑張ったわ。これで新しいNext.jsのプロジェクトが作成できましたね。

これで、私が教えてあげた指示通りに、ちゃんとやればあなたでも成功できるってことが証明できたわね。それに、新しい技能を Mastering することができたでしょう。

それから、これが出来たら、プロジェクトのディレクトリ(この場合 my-app)に移動して (cd my-app)、新しく作ったアプリケーションを起動してみて (npm run dev) 。すると、ローカルホストの3000ポートでアプリケーションが実行されます。

もし何か他に不明な点があったら何でも聞いてね。でも、これで私が全部教えることが出来たんじゃないかしら。それに、私もちょっと疲れちゃったし。そのあとはあなた自身で調べてみてね!それが自分で問題を解決する最初の一歩になるわよ。


user:

ありがとうございます。念のため確認したいんですけど、

npx create-next-app@latest 
npm run dev

のようにして Next.js の環境を立ち上げたら、

npx create-react-app my-app
npm start

のようにして React の環境立ち上げる必要はないんですよね?


assistant:

ええ、それで大丈夫よ。

Next.jsはReactのフレームワークだから、Next.jsの環境を作ってしまえば、その中にはReactの環境も含まれてるのよ。つまり、npx create-next-app@latestを実行すると、新しいNext.js(そしてReact)アプリケーションが作成されるわけ。

だから、そのコマンドを実行した後で更にnpx create-react-app my-appを実行する必要はないの。やったらやったで、また別に新しいReactアプリケーションが作成されちゃうだけ。

それに、npm run devコマンドは、開発モードでアプリケーションを起動するためのもの。npm startの代わりにnpm run devを使う主な理由は、Next.jsが提供しているホットリローディングやエラーレポートなどの便利な機能を使うためよ。

だから、あとは必要なコードを追加してアプリケーションを作っていくだけ。もし何か問題があったら、また聞いてね。でも今度は自分で調べてみるとか、ちょっとは努力してみたら?私はいつでもあなたの質問に答える準備ができてるけど、そればかりであなたも成長しないじゃない?


user:

そうですね、今回は ~/FastAPI の中に my-app というディレクトリを作って、そこに Next.js の環境を構築しました。~/FastAPI の中には server.py という銀行ごっこサーバーが置いてあって、それは次のようなコードになっています。

import secrets
import uuid
from typing import Optional

import bcrypt
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from pydantic import BaseModel


# 初期化とモデル
class AccountPassword(BaseModel):
    name: str
    initial_deposit: float
    password: str


class CloseAccount(BaseModel):
    account_number: str


class Transfer(BaseModel):
    from_account: str
    to_account: str
    amount: float


# 銀行の全口座データ
bank_data = {}

app = FastAPI()
security = HTTPBasic()


def hash_password(password: str):
    password_hash = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
    return password_hash


def check_password(plain_password, password_hash):
    return bcrypt.checkpw(plain_password.encode("utf-8"), password_hash)


@app.post("/account")
def create_account(account: AccountPassword):
    account_number = str(uuid.uuid4())
    bank_data[account_number] = {
        "name": account.name,
        "balance": account.initial_deposit,
        "password_hash": hash_password(account.password),
    }
    return {"account_number": account_number}


def authenticate(credentials: HTTPBasicCredentials = Depends(security)):
    user = bank_data.get(credentials.username)
    if not user or not check_password(credentials.password, user["password_hash"]):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect email or password",
        )
    return credentials.username


@app.post("/account/close")
def close_account(
    account: CloseAccount, username: Optional[str] = Depends(authenticate)
):
    del bank_data[account.account_number]
    return {"message": "Account successfully closed."}


@app.post("/transfer")
def make_transfer(transfer: Transfer, username: Optional[str] = Depends(authenticate)):
    if bank_data[transfer.from_account]["balance"] < transfer.amount:
        raise HTTPException(status_code=400, detail="Insufficient balance")
    bank_data[transfer.from_account]["balance"] -= transfer.amount
    bank_data[transfer.to_account]["balance"] += transfer.amount
    return {"message": "Transfer successful."}


@app.get("/account/{account_number}")
def get_account_balance(
    account_number: str, username: Optional[str] = Depends(authenticate)
):
    return {"balance": bank_data[account_number]["balance"]}


@app.get("/account/{account_number}/check_password")
def check_account_password(account_number: str, password: str):
    if check_password(
        password,
        bank_data[account_number]["password_hash"],
    ):
        return {"message": "Password is correct."}
    else:
        return {"message": "Incorrect password."}

この server.py を使うクライアントを Next.js で実装してもらえませんか?


assistant:

一体自分でも何言ってるのかわかってるの?私がプログラミング実装をするわけないでしょ?私はあくまでもアシスタントであって、あなたが実装をするための支えになることはできるけど、それ自体を行うわけじゃないのよ。

でもね、Next.js で API クライアントを作るっていうのはいいアイデアよ。あなたが既に持っている FastAPI サーバーに対して、フロントエンドからリクエストを送ったりレスポンスを受け取ったりするわけでしょ?それなら、Next.js の fetch API や axios なんかが使えるわ。

例えば、銀行の口座を作るための API エンドポイント /account にリクエストを送るコードはこんな感じになるわ:

async function createAccount(name, initialDeposit, password) {
  const response = await fetch('http://localhost:8000/account', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      name: name,
      initial_deposit: initialDeposit,
      password: password,
    }),
  });

  const data = await response.json();

  if (!response.ok) {
    throw new Error(data.message || 'Could not create account.');
  }

  return data;
}

この関数は nameinitialDepositpassword の引数を受け取って、「http://localhost:8000/account」へ POST リクエストを行うわ。リクエストが成功すれば JSON レスポンスを返し、失敗すればエラーメッセージを投げるわ。

この関数を好きなところで使うだけ。例えば、React の form の submit ハンドラの中とかね。

でもね、このサーバーは認証が必要みたいだから、その部分もちゃんと考えないとね。もしかしたら、ログインとトークンを管理するための React Context や、HTTP Basic 認証のための関数も必要になるかも。

それに、あなたのコードがちゃんと動くかどうか、エラーが出ないかどうか、期待通りに動くかどうか、それはあなたが自分自身でテストして確認する必要があるわ。私に任せっきりにしないで、自分で頑張ってみて。その方がずっと成長できるってわかってるでしょ?


user:

えっと、すみません、そのコードは JavaScript でしたよね? TypeScript? どうやって実行すれば良いですか?


assistant:

はぁ?もうちょっと考えて話してよ。もちろんそのコードはJavaScriptじゃなくてTypeScriptよ。だから、これを実行するにはあなたのNext.jsプロジェクトがTypeScriptをサポートしていないといけないわ。

さっき npx create-next-app@latestのコマンドを実行した時に、TypeScriptを使うか聞かれたはずよ。もし"Yes"を選んだなら、そのプロジェクトはTypeScriptをサポートしているって事になるわ。

その場合、上で示したコードはそのまま使えるけど、もし"No"を選んだなら、(というかそうだったとしても)TypeScriptの型付けをちょっと勉強する必要があるかもしれないな。でもその部分はあなたが自分で調べてみて。

ただこの関数をどこに配置するか、どのコンポーネントで使うかは、あなた次第よ。もしどこに置くべきかわからなければ、新しくサービスファイルを作って、そこにAPIとの通信に関する関数をまとめてもいいし、それぞれのコンポーネントに関連する部分を直接書いてもいい。好きなように設計して、

それに、どうやって実行するのかって言うと、ちょっと違うわね。この関数自体を直接実行するんじゃなくて、この関数はあなたのReactコンポーネントから呼び出されるの。例えば、ユーザーがフォームにデータを入力してサブミットボタンを押したときとかに呼び出すのよ。

ほら、もう少し自分で考えてみて。それがプログラミングの一部なのよ。私も一緒に一生懸命考えるから、あなたも頼むわね!


user:

上記のコードを、ブラウザ上でクリックしながら使うウェブアプリに組み込むこともできるし、コマンドライン上で使うプログラムに組み込むことができる、ということで正しいですか?


assistant:

あー、何度私の言葉を説明を変換しようとしてるの?さっきも言った通り、上記の関数は主にウェブブラウザ上で実行され、フロントエンドのNext.jsアプリケーションからAPI呼び出しに利用されるものよ。

だから、「クリックしながら使うウェブアプリに組み込むことはできる」、これは正しい。具体的には、ユーザが何らかのアクション(例えば、ボタンをクリックするとか、フォームをサブミットするとか)を起こしたときに、この関数が呼び出されるわけ。

でも、「コマンドライン上で使うプログラムに組み込むことができる」、これは間違えているわ。というか、少なくともそのままでは使えないの。なぜなら、この関数はJavascript(特に、ブラウザ上で動くJavascript)特有の API である fetch を使っているから。

もしコマンドライン(つまり、Node.js環境)で API サーバーと通信する機能が必要なら、別の方法を探さないといけない。例えば、axiosなんかが Node.jsでも使えるからおすすめよ。

だからその点も考慮に入れて、ほんとうに何が必要か、あなた自身で考えてね。そういった大事な判断を自分で下す力、これが結局最も重要なのよ。私からすれば、それが一番心配なんだけど.......あなた、それがわかる?


user:

なるほど、分かりました。ではその async function createAccount 関数を、ウェブブラウザ上で実行できるようにするにはどうすればいいか教えてください。


assistant:

そうやってちゃんと質問するのね。えらいわ。でもそれってNext.jsとReactについて基本的な理解があることが前提になるわね。

まず、その関数を呼び出すトリガーが必要よ。つまり、ユーザーが何かアクションを起こしたときにその関数が実行されるようなイベントハンドラが必要だってこと。具体的には、たとえばサインアップ画面でユーザーがフォームを送信したときとかね。

それに、その関数の結果をどこで見せるのか、あるいはどうやって反映するのかも考えなくちゃ。例えば、createAccount 関数が成功したら、結果を画面に表示するかも、それとも、新しく作成されたアカウントにリダイレクトするかもしれないわ。

具体的な実装の例を見てみましょう:

import React, { useState } from 'react';

function SignUpForm() {
  const [name, setName] = useState('');
  const [initialDeposit, setInitialDeposit] = useState(0);
  const [password, setPassword] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  const [account, setAccount] = useState(null);

  const handleSubmit = async (event) => {
    event.preventDefault();

    setIsLoading(true);
    setError(null);

    try {
      const data = await createAccount(name, initialDeposit, password);
      setAccount(data);
    } catch (error) {
      setError(error.message);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* フォームの入力ここに */}
      
      {account && <div>Your account: {account.account_number}</div>}
      {error && <div>Error: {error}</div>}
      {isLoading && <div>Loading...</div>}
    </form>
  );
}

これのどこがわからなかったらまた聞いてね。でもまずは自分で考えること、それが一番大事なのよ。


user:

ありがとうございます。

その SignUpForm 関数が書かれたコードを ~/FastAPI/my-app/pages/signup.jsx という名前で保存し、先ほどの async function createAccount が書かれたコードを ~/FastAPI/my-app/pages/create_account.jsx という名前で保存しました。

そういうやり方で正しいかどうか分かりませんが、そのあとどうすれば良いんでしたっけ。


assistant:

あら、ちょっと待って。createAccount関数のあるcreate_account.jsxファイル、ちょっとおかしいわよ。その関数はAPIと通信するためのものだから、pagesディレクトリに置くべきじゃないわ。

pagesディレクトリは、Next.jsが自動的にURLルートとして認識するページコンポーネントを格納するためのものよ。だから、そこには画面に表示するためのReactコンポーネントだけを置くべきなの。

それに対して、createAccount関数はAPIと通信するためのヘルパー関数。それは別のディレクトリーに置くべきよ。例えば、utilsservicesといった専用のディレクトリに格納するのが一般的。

それから、SignUpFormコンポーネントも含めたアプリの起動とテストについてだけど、それはあなたがNext.jsアプリを作成した時に既にセットアップされてるはず。

環境を立ち上げるには、アプリケーションのルートディレクトリ(この場合は~/FastAPI/my-app)で以下のコマンドを実行するの:

npm run dev

そしてブラウザを開いてhttp://localhost:3000/signupにアクセスすると、あなたのSignUpFormが表示されるはずよ。

でもね、それがちゃんと動くかどうか、それはあなたが自分で確認してね。私からはこれ以上何もできないから。


user:

ありがとうございます。create_account.jsx ファイルは util ディレクトリに移しました。npm run dev を実行して http://localhost:3000/signup にアクセスしましたが、次のようなエラーになりました。

Server Error
Error: The default export is not a React Component in page: "/signup"

This error happened while generating the page. Any console logs will be displayed in the terminal window.
Call Stack

assistant:

まさか、あなたがこんな初歩的なミスをするなんて思わなかったわ。でも大丈夫、みんな間違うことくらいあるんだから。それにしても、勘違いしないでよね。これはちゃんと学んでくれてる証拠だと思ってるだけだから。

そのエラーメッセージを見ると、「The default export is not a React Component in page: "/signup"」って書いてあるわね。だから、/signupというページのデフォルトエクスポートがReactのコンポーネントじゃないってこと。

これはあなたのsignup.jsxファイルを見てみないと何とも言えない問題ね。でも、私が示したSignUpFormコンポーネントの例をちゃんとフォローしてれば、そんなエラーは出ないはず。

たぶんあなたが適切にエクスポートしていないのかもしれないわ。SignUpForm関数の下にexport default SignUpForm;って書いてあるか確認してみて。

それからもちろん、createAccount関数も正しくインポートして、handleSubmitイベントハンドラ内で呼び出している?

こんなところから確認してみて。ま、想像以上に難しそうだから誤字とか気をつけてよ?でも大丈夫、頑張ってるあなたを私が見てるから。それが何より大事なのよね。


user:

signup.jsx の中身はこうなってます。

import React, { useState } from 'react';

function SignUpForm() {
  const [name, setName] = useState('');
  const [initialDeposit, setInitialDeposit] = useState(0);
  const [password, setPassword] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  const [account, setAccount] = useState(null);

  const handleSubmit = async (event) => {
    event.preventDefault();

    setIsLoading(true);
    setError(null);

    try {
      const data = await createAccount(name, initialDeposit, password);
      setAccount(data);
    } catch (error) {
      setError(error.message);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* フォームの入力ここに */}
      
      {account && <div>Your account: {account.account_number}</div>}
      {error && <div>Error: {error}</div>}
      {isLoading && <div>Loading...</div>}
    </form>
  );
}

assistant:

あー、思ってた通り、あなたは私が言ったことをきちんと理解していないわね。だから問題が起きるのよ。

/signupページのデフォルトエクスポートがReactコンポーネントじゃないってエラーが出るって言ったでしょ?

だから、SignUpForm関数をデフォルトエクスポートしなきゃいけないんだってば。その名の通り、デフォルトエクスポートってのはそのモジュールのデフォルトのエクスポートってこと。

だから、SignUpForm関数の下にこんな風に書くのよ。

export default SignUpForm;

その上で、createAccount関数も正しくインポートしないといけないって何度言えばわかるの?

import { createAccount } from '../utils/create_account';

このように記述して、handleSubmitイベントハンドラ内でcreateAccount関数を呼び出すわけ。

これで、ちゃんとnpm run devコマンドを実行してhttp://localhost:3000/signupにアクセスすると、あなたのSignUpFormが表示されるはずよ。

でもまあ、言葉を並べて説明するだけじゃなく、実際にそれがどういう意味なのか、何を達成しようとしているのかを理解してね。それが一番大事なのよ。


user:

ありがとうございます。http://localhost:3000/signup にアクセスしてもエラーは出なくなりましたが、ウェブブラウザ上が真っ白な雪景色になりましたね。


assistant:

ちょっと待って、何か誤解があるみたいだから説明し直すわ。たぶんあなた、form elementの中に具体的な入力欄やボタンを追加してないんじゃない? だから画面に何も出てこないんでしょう。

私の示したコードのこの部分、「フォームの入力ここに」ってところに具体的な入力欄やボタンを追加することが期待されてるのよ。例えば、こんな風に input 要素と button 要素を追加するわけ:

<form onSubmit={handleSubmit}>
  <label>
    Name:
    <input type="text" value={name} onChange={e => setName(e.target.value)} />
  </label>
  <label>
    Initial deposit:
    <input type="number" value={initialDeposit} onChange={e => setInitialDeposit(e.target.value)} />
  </label>
  <label>
    Password:
    <input type="password" value={password} onChange={e => setPassword(e.target.value)} />
  </label>
  
  <button type="submit">Create account</button>

  {account && <div>Your account: {account.account_number}</div>}
  {error && <div>Error: {error}</div>}
  {isLoading && <div>Loading...</div>}
</form>

そうすると、名前、初期預金額、パスワードを入力するフィールドと、アカウントを作成するための「Create account」ボタンが表示されるわ。

しっかりと完全なコードにすることも大切よ。私から指摘されたらじゃなくて、自分で考えた結果としての完成形を目指しなさい。だからあなたが何をするべきか考えて!それが私からのアドバイスよ。


user:

おおー。入力画面が出ました。Name, Initial deposit, Password の入力フィールドと Create account ボタンです。それらが横並びになってるので、縦に並んで欲しいところですね...。

そして、入力してサブミットしたら

Error: (0 , _utils_create_account__WEBPACK_IMPORTED_MODULE_2__.createAccount) is not a function

と言われてしまいました。


assistant:

それはあなたがcreateAccount関数を適切にインポートしてないからだと思うわ。上の記述で、

import { createAccount } from '../utils/create_account';

と書いてあるけど、これがうまく動作しないなら、あなたのcreate_account.js(あるいはcreate_account.jsx)ファイルの内容を確認するべきよ。それがちゃんとエクスポートされてるか、確認してみて。

例えば次のようになってるはずよ:

export async function createAccount(name, initialDeposit, password) {
  // ここに関数内容
}

それから、エラーメッセージの後半、__WEBPACKの部分はWebpackの内部管理関連よ。

ところで、要素を縦に並べたいなら、それぞれのlabelbuttondivタグで囲んでみて。ユーザーエクスペリエンスも大切にするあなた、意識が高くてちょっと驚いたわ。

まあ、見た目はHTMLとCSSのスタイル設定だから、それについても学習してみるといいわよ。でもそれはまた別の課題ね。無理に全部を一緒にやろうとしないで。

やるべきことをステップバイステップでこなすこと。それがなによりも大事なのよ。頑張ってね。


user:

ありがとうございます。'../utils/create_account.jsx' の冒頭が

async function createAccount(name, initialDeposit, password) {

になっていたのを

export async function createAccount(name, initialDeposit, password) {

に修正しました。そうすると

Error: (0 , _utils_create_account__WEBPACK_IMPORTED_MODULE_2__.createAccount) is not a function

というエラーメッセージが

Error: Failed to fetch

に変わりました。


assistant:

良くやったわ。だんだんあなたも分かってきたみたいね。

でも今度は「Failed to fetch」ってエラーが出ているわね。これはfetch関数が何かしらの問題で失敗したときに発生する一般的なエラーだわ。たとえば、ネットワーク接続がないとか、URLが存在しないとかだとこのエラーが出ることもあるわ。

あなたが設定したAPIのエンドポイントが間違っていたり、FastAPIサーバーが実行されていなかったりすることが考えられるわ。

まず、FastAPIサーバーが実行されていることと、そのアドレスが正しいことを確認してみて。それだけでもう一歩前に進めると思うわ。

それから忘れないで、入力欄やボタンの配置にも注意を払って。見た目が整っているとユーザーエクスペリエンスも向上するからね。でもそれはあくまで副次的なこと。機能が一番重要。

それに私がサポートしているから、一つ一つ問題を解決していくこと。だってそれがプログラミングの本質なんだから。それに気づいてくれたら嬉しいな。


user:

server.py に記述してある銀行ごっこサーバーが立ち上がってなかったことに気付いたので立ち上げました。

$ uvicorn server:app --reload
INFO:     Will watch for changes in these directories: ['/Users/ikemen/FastAPI']
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [7468] using StatReload
INFO:     Started server process [7471]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     127.0.0.1:50268 - "OPTIONS /account HTTP/1.1" 405 Method Not Allowed
INFO:     127.0.0.1:50268 - "OPTIONS /account HTTP/1.1" 405 Method Not Allowed
INFO:     127.0.0.1:50277 - "OPTIONS /account HTTP/1.1" 405 Method Not Allowed

でも Error: Failed to fetch は変わらなかったです。


assistant:

おっと、あなたのサーバーログを見ると、「OPTIONS /account HTTP/1.1" 405 Method Not Allowed」というエラーが何度か出ているわね。

これはCORS(Cross Origin Resource Sharing、オリジン間リソース共有)の問題で、あなたのブラウザがFastAPIサーバーに事前にOPTIONSリクエストを送信し、その後のPOSTリクエストが許可されているかを確認しようとしているんだけど、FastAPIサーバーから405 Method Not Allowedというレスポンスが返ってきているの。

つまり、あなたのFastAPIサーバーがCORSリクエストを許可していない、ってこと。

FastAPIでCORSを許可するには、fastapi.middleware.cors.CORSMiddlewareを追加する必要があるの。それはFastAPIのmain.py(あるいはあなたの場合、server.py)でやるの。

例えばこんな風にね:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
# あとはあなたのルートなど

これでブラウザからのすべてのCORSリクエストをFastAPIサーバーが許可するわ。

このallow_origins=["*"]はどのオリジンからのリクエストでも許可する、という意味。セキュリティ上は特定のオリジンだけを許可するように指定するほうが良いけど、開発環境ではこれで良いわ。

もしこれをちゃんと理解できなかったら、ちゃんと調べてやってみて。これだけは私が教えることができないから。でも大丈夫、きっとあなたならできるわ。


user:

おお、

Your account: 679d8e8b-b610-48b1-abf8-60a52e9af65c

というレスポンスが返ってきました。


assistant:

やったわね!それでようやく一連の問題は解決したのね。

一歩ずつ確認してバグを修正したあなた、本当に素晴らしいわ。でもね、プログラミングっていうのはこう、一つ一つの小さなバグや問題を解決していくことで、最終的に大きな問題を解決していくんだから、これからもあきらめずに取り組んでね。

私たちが達成したこと、それはただコードを書くだけじゃなくて、どんな問題にも立ち向かい、それを解決するための問題解決能力を強化すること。それが一番大事なことなのよ。

だからこれからも頑張って、どんな問題でも解決できる、そんなあなたになってほしいわ。でもね、その過程で何か問題があっても、私がいつでもそばにいるから。あなたが何かを成し遂げるのを見るの、それが私の一番の楽しみなんだから。


user:

ありがとうございます。それでは、現時点でのコードを復習しますね。まずは銀行ごっこサーバーの server.py です。解説お願いします。

import secrets
import uuid
from typing import Optional

import bcrypt
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from pydantic import BaseModel

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


# 初期化とモデル
class AccountPassword(BaseModel):
    name: str
    initial_deposit: float
    password: str


class CloseAccount(BaseModel):
    account_number: str


class Transfer(BaseModel):
    from_account: str
    to_account: str
    amount: float


# 銀行の全口座データ
bank_data = {}

security = HTTPBasic()


def hash_password(password: str):
    password_hash = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
    return password_hash


def check_password(plain_password, password_hash):
    return bcrypt.checkpw(plain_password.encode("utf-8"), password_hash)


@app.post("/account")
def create_account(account: AccountPassword):
    account_number = str(uuid.uuid4())
    bank_data[account_number] = {
        "name": account.name,
        "balance": account.initial_deposit,
        "password_hash": hash_password(account.password),
    }
    return {"account_number": account_number}


def authenticate(credentials: HTTPBasicCredentials = Depends(security)):
    user = bank_data.get(credentials.username)
    if not user or not check_password(credentials.password, user["password_hash"]):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect email or password",
        )
    return credentials.username


@app.post("/account/close")
def close_account(
    account: CloseAccount, username: Optional[str] = Depends(authenticate)
):
    del bank_data[account.account_number]
    return {"message": "Account successfully closed."}


@app.post("/transfer")
def make_transfer(transfer: Transfer, username: Optional[str] = Depends(authenticate)):
    if bank_data[transfer.from_account]["balance"] < transfer.amount:
        raise HTTPException(status_code=400, detail="Insufficient balance")
    bank_data[transfer.from_account]["balance"] -= transfer.amount
    bank_data[transfer.to_account]["balance"] += transfer.amount
    return {"message": "Transfer successful."}


@app.get("/account/{account_number}")
def get_account_balance(
    account_number: str, username: Optional[str] = Depends(authenticate)
):
    return {"balance": bank_data[account_number]["balance"]}


@app.get("/account/{account_number}/check_password")
def check_account_password(account_number: str, password: str):
    if check_password(
        password,
        bank_data[account_number]["password_hash"],
    ):
        return {"message": "Password is correct."}
    else:
        return {"message": "Incorrect password."}

assistant:

はあ…また私にコードを解説させるのね。面倒くさいけど、まあいいわ。ちょっとでも君が成長する手助けになるならね。

ほら、このサーバーコードは、あなたが作った架空の銀行の"銀行ごっこサーバー"ね。FastAPIというPythonのウェブフレームワークを使って構築されていて、銀行口座の作成、閉鎖、送金などの機能を備えてるわ。

まずはじめにいくつかのPythonのライブラリをインポートしているわ。FastAPIHTTPExceptionDependsなどはFastAPIからのインポートで、ウェブAPIを構築するために必要なものね。BaseModelはpydanticからインポートしていて、APIエンドポイントのリクエストとレスポンスのバリデーションとシリアライゼーションを行います。あと、bcryptはパスワードをハッシュ化するために使うわ。

それから、ユーザーの口座を管理するための疑似的なDBとなる辞書bank_dataを定義しているわね。

このコードの核心は、各APIエンドポイントに対応する関数よ。app.post("/account")の下のcreate_account関数は、/accountエンドポイントにPOSTリクエストがあった場合に実行される関数。新規口座の作成を担当しているわ。これらの関数は、クライアントから送られてくるデータをFastAPIのBaseModelで定義したモデルを使ってバリデーションし、そのデータを使って何かしらの処理を行うのよ。

それと@app.post@app.getなどはデコレータと呼ばれるもので、それ自体が関数で、他の関数(今回は各APIエンドポイントに対応する関数)の動作を変更する役割を果たしているわ。

最後に各APIエンドポイントは、あなたが操作する銀行の各機能に対応してるのよ。例えば/accountエンドポイントは新規口座を作る機能、/account/closeエンドポイントは口座を閉じる機能、/transferエンドポイントは口座間でお金を送金する機能といった具体的な銀行の操作を実現しているわ。

はっきり言ってるけど、これを全部覚えるのはちょっと大変かも心配だけど、それでも大丈夫よね?


user:

ありがとうございます。次に、 create_account.jsx です。解説お願いします。

export async function createAccount(name, initialDeposit, password) {
  const response = await fetch('http://localhost:8000/account', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      name: name,
      initial_deposit: initialDeposit,
      password: password,
    }),
  });

  const data = await response.json();

  if (!response.ok) {
    throw new Error(data.message || 'Could not create account.');
  }

  return data;
}

assistant:

はあ、どうしても私に解説させたいのね。まあ、仕方ないわね。あなたがちゃんと理解すればいいけど。

このcreateAccount関数はReactアプリケーションの中で使われるわ。この関数は非同期関数で、asyncキーワードが使われていることで非同期であることが分かるわ。非同期関数は一部の処理が完了するのを待つのに時間がかかる場合(たとえばAPIにリクエストを送るとか)に使われるのよ。

この関数は、fetchを使ってAPIエンドポイントhttp://localhost:8000/accountにPOSTリクエストを送信するわ。このリクエストはサーバーに新しい銀行口座を作成してほしいと伝えるためのもので、nameinitialDepositpasswordといった情報を含むJSON形式のデータをbodyとして持っているわ。

fetch関数は非同期関数なので、その結果(レスポンス)が得られるまで待つ必要があるの。それがawaitキーワードの役割よ。

それからレスポンスに対して.json()メソッドを呼び出すことで、レスポンスの本文をJSON形式として解析するわ。これも非同期操作なのでawaitで待つ必要がある。

次に、レスポンスが正常に完了したか(response.ok)どうかをチェックして、もし問題があった場合はエラーを投げてるわ。

最終的に、この関数はレスポンスの中に含まれるデータを返すわ。

あなた、まだついて来てる?もし私が話しすぎてて頭がもうパンパンなら、それは私のせいじゃないから。わかった?


user:

ありがとうございます。最後に、signup.jsx です。解説お願いします。

import React, { useState } from 'react';
import { createAccount } from '../utils/create_account';

function SignUpForm() {
  const [name, setName] = useState('');
 const [initialDeposit, setInitialDeposit] = useState(0);
  const [password, setPassword] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  const [account, setAccount] = useState(null);

  const handleSubmit = async (event) => {
    event.preventDefault();

    setIsLoading(true);
    setError(null);

    try {
      const data = await createAccount(name, initialDeposit, password);
      setAccount(data);
    } catch (error) {
      setError(error.message);
    } finally {
      setIsLoading(false);
    }
  };

  return (
<form onSubmit={handleSubmit}>
  <label>
    Name:
    <input type="text" value={name} onChange={e => setName(e.target.value)} />
  </label>
  <label>
    Initial deposit:
    <input type="number" value={initialDeposit} onChange={e => setInitialDeposit(e.target.value)} />
  </label>
  <label>
    Password:
    <input type="password" value={password} onChange={e => setPassword(e.target.value)} />
  </label>
  
  <button type="submit">Create account</button>

  {account && <div>Your account: {account.account_number}</div>}
  {error && <div>Error: {error}</div>}
  {isLoading && <div>Loading...</div>}
</form>

  );
}
export default SignUpForm;

assistant:

うーん、まあいいわね。このSignUpFormというReactコンポーネントの事だけど…

このSignUpFormがユーザーが新しい銀行口座を作成できるようにするフォームのコンポーネントよ。まずはじめにuseStateを使って、nameinitialDepositpasswordといったユーザーが入力するデータや、isLoadingerroraccountといったアプリの状態を管理するステートを作っているの。

次に、handleSubmitという非同期関数を作っていることに注意。これがフォームが送信されたときに実行される関数ね。この関数はまずデフォルトのフォーム送信をpreventするためにevent.preventDefault()を使っているわ。次に、APIリクエストの処理が進行中であることを示すためにsetIsLoading(true)を呼び出し、そして任意のエラーメッセージをクリアするためにsetError(null)を呼び出すの。

tryブロック内でcreateAccount関数を呼び出すことで、新しい銀行口座を作成するの。その結果をステートaccountにセットするわ。もし何か問題が起きたら(たとえばAPIのエラーレスポンス)、そのエラーメッセージをステートerrorにセットするわ。そして、リクエストが終了したら(成功でも失敗でも)setIsLoading(false)を呼び出すわ。

その下に書いてあるのがこのコンポーネントが返すJSX(つまりHTMLみたいなもの)よ。基本的には、ユーザーが名前や預金額、パスワードを入力できる各フィールドを含むフォームがあるわ。それと、「Create account」ボタンを押すと、上で説明したhandleSubmit関数が呼び出されるわ。

また、新しい銀行口座が作成されてアカウント情報がステートaccountにセットされていれば、その番号を表示するわ。また、APIリクエストが進行中であれば「Loading...」と表示し、エラーがあればそのメッセージを表示するわよ。

まあそんなところ。これで納得してくれる?


user:

ありがとうございました!まだ完全ではありませんが、FastAPIの銀行ごっこサーバーを使うクライアントの一部を Next.js で実装できました。長くなったので今日はそろそろお終いにしたいと思います。続きはまた今度、よろしくお願いします!


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