背景
去年だとおもいますが、とある企業で面接をうけまして、その際に技術力を見るテストとしてRust + React + Next.jsでシンプルなwebアプリを作りました。数日で面接用に作ったものなので完成度はあまり高くないと思いますが、なるべく本番利用に耐えられるようにかんがえてつくったものなのでActix webを使ってなにか作りたいという人には参考になるとおもいます。Next.jsでSSGしており、読み込みも早いはずです。
URL: https://github.com/lechatthecat/assignment
開発をするたびにDocker composeのyamlファイルだったり、APIハンドラーだったり、Nextjsの設定だったり、
毎回いちいち同じような実装をするのが面倒だし、「あれ、あれってどうやるんだっけ」ってなることもあるので、とりあえず動いてはいるこれをもとに開発をすると早いと思います。
海外の人が面接官だったので中身は全部英語になっていますが、ご了承ください。
その企業には結局いかなかったのですが、せっかくつくったこのレポジトリを眠らせたままにしておくのはもったいないのでみんなに共有します。その会社に関連しそうな部分は削除してGitのヒストリーからも消してます。
ただし、面接の条件として、DBのORM library(Diesel等)を一切つかうなとのことだったのでORM系は一切入っていません。これを参考にする際にはそのあたりに留意をお願いします。実際にこれをもとに開発をする際にはDiesel等の導入が必要になります。
あとテストコードの書き方はあまり参考にしないでください。。あくまで時間がない中で作ったものを参考のために共有ということでお願いします。
なおRustのweb frameworkのActix webはそうとう速いらしく、数年前は下記のベンチマークでトップにいました(うろ覚え)。
https://www.techempower.com/benchmarks/#hw=ph&test=fortune§ion=data-r22
いまはだいぶ順位が下になっていますが、Actix ecosystemのactix-httpはそれでも2024/8/15現在で12位にいます。たぶんそこそこ成熟したframeworkとしてはかなり速い部類なんじゃないかな。
Actix webはググっても色々な情報がでてきます。
https://jitera.com/ja/insights/22518#:~:text=Actix%20Web%E3%81%AF%E3%80%81Rust%E8%A8%80%E8%AA%9E%E3%81%A7%E9%96%8B%E7%99%BA%E3%81%95%E3%82%8C%E3%81%9F%E5%BC%B7%E5%8A%9B,%E3%82%92%E5%AE%9F%E7%8F%BE%E3%81%97%E3%81%A6%E3%81%84%E3%81%BE%E3%81%99%E3%80%82
使い方
使い方はREADME.mdにも書いてありますが、
$ git clone https://github.com/lechatthecat/assignment
$ cd assignment
$ docker compose up --build -d
そうするとRustのリリースビルドが始まって以下のURLからログイン画面が開けるようになります。
http://localhost/login
この情報でログインを行ってください。
name: test_user1
password: password
開発用に
なお、dev側のデバッグ用のdocker-compose_dev.ymlを使う場合はこんな感じになると思います。
$ docker compose -f docker-compose-dev.yml up -d --build
何をするアプリなのか
構成
Actix webによるAPIサーバーと、それを呼び出すReact + Next.jsアプリにわかれています。
どういうAPIが使用可能なのかはAPI handlerをみてください。
https://github.com/lechatthecat/assignment/blob/main/simple_restaurant_api/src/api/api_handler/handlers.rs
APIサーバー, nginx, postgresqlをDocker composeでそれぞれコンテナを作成して立ち上げるようにしています。frontend
フォルダーのコードはnginxのコンテナ内に自動で同期され、nginxがそれを返してます。
ただし/api
が先頭につくリクエストはすべてnginxがActix webサーバーにリダイレクト(リバースプロキシ)しています。それ以外のリクエストのみnginxコンテナ内のfrontend
を参照します。
概要
現在テーブルが10個あるレストランで使うことを想定したシンプルなSPA(Next.jsでSSGしているのでSPAという名称が正確なのかよくわからないですが)で、ウェイターがそれぞれタブレットをもって、タブレットから呼び出して、注文を受ける際にまずどのテーブルの注文なのか、テーブルを選択します。
選んだらその画面の現在の受領済みオーダー一覧画面にいきます。
Menuを押すとメニュー一覧ページに行くので、そこからオーダーされたメニューをクリックします。
メニューには料理の値段と準備ができるまでの想定時間がかかれています。
Add this to order
を押すと受領済みオーダーにそのメニューを追加できます。
追加すると準備完了までのリアルタイムのカウントダウンが始まります。ゼロになったらウェイターがキッチンに料理を取りに行ってそのテーブルのお客さんに料理を出す、という感じの使い方を想定しています。
だいたい何時ころに料理の準備が完了するかの時刻も見れるようになっています。
なんだよ機能自体は大したことないな、って思いましたね。繰り返しますが面接用に数日で作ったので最低限しか実装していないです。。
追加でやったほうがいいこと
To DOとして、以下があります。
- APIのドキュメントが一切ないのでSwaggerを導入してそのあたりドキュメント化をしたほうがいいとおもっています
- RedisによるAPI側の処理のキャッシュ, Cloud flareなどのCDNキャッシュの導入もやるとサーバー負荷が下がるはずです。今回は内部の人が使うようなので大きな負荷はかからないはずであり、そのあたりはスキップしちゃいましたが、一般に公開するようなアプリの場合はキャッシュ処理の導入も必要になるはずです。
- 負荷がかかるようなサービスをホストする場合では、APIサーバーのコンテナ, frontend用のコンテナ, ロードバランサのコンテナは分けて、docker composeじゃなくてkubernetesでコンテナの管理をするとスケールアウト・スケールインが楽でいいと思います。
-
docker-compose.yml
のmyrust
のvolumeが以下のようにファイルごとにbindされています。これはtarget
フォルダをbindしてしまうと、せっかくコンテナ側でbuildしたtarget
フォルダがホスト側の空のtarget
フォルダで上書きされてしまうためです。ここは開発用にローカルで起動したときの利便性を考えると、ファイルごとではなくフォルダごとの同期のほうがいいかもしれないです。たとえば同期はフォルダ単位にしてbuild用のshell scriptをコンテナ立ち上げごとに自動実行するなど。(まあ必須な対応じゃないと思いますし、開発用には開発用のdocker-compose.yml
を使うようにするとかでも解決できます。build用のshell scriptは、Run
ではなくcommand
で実行する場合、docker compose up
コマンドが完了してもスクリプトのbuild
コマンドが終わるまでサービスが404になってしまうので面接官が混乱するかと思い採用しませんでした。。ん?でも、書いてて思ったけどホストマシンのディレクトリのマウントって本番へのデプロイなら必要ないかも。やっぱりyamlを本番と開発で分けるのが一番か)
volumes:
# log
- ./logs:/simple_restaurant_api/log
# Rust code
- ./simple_restaurant_api/src:/simple_restaurant_api/src
- ./simple_restaurant_api/Cargo.lock:/simple_restaurant_api/Cargo.lock
- ./simple_restaurant_api/Cargo.toml:/simple_restaurant_api/Cargo.toml
-
frontend
ではNextjsのoutディレクトリをコミットしてしまっていますが、本当はnpm run buildをcontainer立ち上げのたびに行い、outディレクトリをコミットするのではなく生成する方式のほうがいいです。主にAPI側の実装をみる面接だったということで、面接官にいちいちnpm run build
コマンドを打ってもらうのもどうかなと思い。。本番で使うならここは修正したほうがいいと思います。(たとえば生成用のshell scriptをcommand
で自動実行するようにするなど。Run
でやる場合はファイルごとの同期に変えないとだめかも) - 本当はオーダーを実際に厨房に伝える前に、お客さんに「〜という注文でよろしいですか?」という確認が入るはずなので、確認をするためのオーダーを仮に受ける(厨房には伝えない)という機能があったほうがいいんですが、フロント側の開発になりそうだし、本来はAPI側をみるための試験なのでスキップにしました。
- Production前提なのにunwrap関数をテスト以外で使っちゃってますがRustではお行儀の悪い書き方なのでここは直したほうがいいです。unwrapのかわりに使うべきなのはexpect関数(エラー時に任意のメッセージを一緒になげられる)、あるいは外部処理を呼び出している等Productionでもエラーになりうるようなところはmatch構文によって明示的にエラー処理をしたり、またはor_else関数を使うのもいいでしょう。