❤️‍🔥

フロントエンジニアがRustのaxumでTODOアプリに入門するンゴ①

2024/06/09に公開

バックエンドも興味がある年頃なのですが、Rustを軽く入門したら「結構好きだなあ」って思ったのでTODOアプリから作成してみます。

僕は普段はwebサービスのフロントエンドを担当していて、天から降ってきたAPIからjsonを受け取って表示させている身ですが、APIがどういったものかについても解き明かせればと考えています。

途中だけどリポジトリはこちら↓
https://github.com/ask-nugey/rust_todo_app

主な目的

ヌギーはフロントエンジニアだから、バックエンドの基礎的なことも学習することが目的!
個人的にはいわゆる "おまじない"は嫌い(というか気になり過ぎて進めない)ので、そこらへんも適度にわかるようにしたいです!

今回はヌギーの思考過程も含めて記事にしていくよん。

技術選定とか

  • 言語:
    • 言わずもがな、Rust!
    • PHP、Goとかも候補あったけど、Rustはパッと見た感じヌギー的には"わかりやすい"言語だしヌギーが書いてみたい言語1位だから!(Goもだけど型安全なのもポイント高し)
    • (Goも今後やってみたい)
  • Webフレームワーク:
  • データの保存方法:
    • 今回はDBは使わないで、TODOのタスクのデータは簡易的な実装でアプリケーション内でデータを保持する方法を選定したよ!
      - データの形式はメモリ内にデータを一時的に保存する形にしました(データの永続的には保存されない)
    • データベースを採用しなかったのは、今回はまず手軽にバックエンドの全体像を理解したかったから!
    • 今後はRDB(PostgreSQLやMySQL)を使った本格的なアプリケーションへのブラッシュアップにも挑戦してみたい!
  • デプロイとか
    • 一旦はローカルで完結させるよ!
    • 次はデプロイもしていきたい!

axumのexamplesにあったTODOアプリを参考にするンゴ

まずは良いコードから パクる 真似るのが基本だと思うので、axumのexamplesにあるaxum/examples/todos/を理解していく戦法でいきます。

十分小さいコードだとは思いますが、ヌギーの現レベルではすんなりわからないのでチャッピー(ChatGPT)に聞きつつ、最小限のコードかつ段階的にこのコードに辿り着くように進めていきます。

元のコードを流れを大雑把に解析

チャッピー(ChatGPT)に axum/examples/todos/src/main.rs のコードを食わせて流れを聞くとこんな感じになった↓

以下は、このコードの主な流れを簡単に説明したものです。

1. Initialize Tracing Subscriber
    |
    V
2. Create Database (Db)
    |
    V
3. Compose Routes
    |
    V
4. Add Middleware
    |
    V
5. Start Server and Listen on Port 3000
    |
    V
6. Handle Requests
    |
    |---> GET /todos ----> Fetch list of Todos
    |
    |---> POST /todos ---> Create a new Todo
    |
    |---> PATCH /todos/:id --> Update a specific Todo
    |
    |---> DELETE /todos/:id --> Delete a specific Todo

### 説明

1. **Initialize Tracing Subscriber**:
    - トレーシングサブスクライバーを初期化し、ログ設定を行います。

2. **Create Database (Db)**:
    - 共有可能なデータベース(`Db`)として`HashMap`を`RwLock`でラップし、`Arc`で参照カウントを管理します。

3. **Compose Routes**:
    - 各エンドポイント(`/todos`、`/todos/:id`)に対するルートを設定します。

4. **Add Middleware**:
    - エラーハンドリング、タイムアウト、およびトレースレイヤーを追加して、すべてのルートに対するミドルウェアを設定します。

5. **Start Server and Listen on Port 3000**:
    - サーバーを起動し、ポート3000でリクエストを待ち受けます。

6. **Handle Requests**:
    - 各種リクエスト(GET、POST、PATCH、DELETE)に応じて処理を行います。
    - `GET /todos`:Todoリストを取得します。
    - `POST /todos`:新しいTodoを作成します。
    - `PATCH /todos/:id`:特定のTodoを更新します。
    - `DELETE /todos/:id`:特定のTodoを削除します。

まず思ったのは、「1. Initialize Tracing Subscriber」はログ設定ということなので、必須じゃないだろう。最後に設定してもいいんじゃね?と思いました。

他にも必須ではないものを省けば最小限なコードになるだろうと考え、チャッピーに必須な部分を聞き出しました↓

以下の各ステップについて、それぞれの必須性を説明します。

1. **Initialize Tracing Subscriber**
    - **必須ではありません**。アプリケーションの動作を追跡しデバッグするためのもので、削除可能です。

2. **Create Database (Db)**
    - **必須です**。データベースはTodo項目を保存および管理するために必要です。

3. **Compose Routes**
    - **必須です**。ルートはエンドポイントを定義し、HTTPリクエストを適切なハンドラーにルーティングするために必要です。

4. **Add Middleware**
    - **必須ではありませんが、推奨されます**。ミドルウェアはエラーハンドリング、タイムアウト、トレースなどの追加機能を提供しますが、基本的な機能には不要です。

5. **Start Server and Listen on Port 3000**
    - **必須です**。サーバーを起動し、クライアントからのリクエストを受け付けるために必要です。

6. **Handle Requests**
    - **必須です**。リクエストハンドラーは各エンドポイントの機能を実装し、クライアントのリクエストに応答するために必要です。

おk。

まずは、「1. Initialize Tracing Subscriber」と「4. Add Middleware」の機能を省いたコードを完成させることを目標にしますか。

ちょっとずつコードを書いていく戦法(やっとここから実際にコードを書いていくよ)

まずは、Rustのプロジェクトのセットアップ(ハローワールドする)

ここで出来上がるコード↓
https://github.com/ask-nugey/rust_todo_app/commit/7055666119686fd15ccbf7eb72f6d87958d194bc

Rustだとcargoコマンドで新規プロジェクトを作成したり、コンパイルするけどそこらへんの導入はヌギーのコードを参考にしてね↓
https://ask-nugey.com/posts/rust-build-environment#実際にrustup-initインストーラーを実行セットアップ

①ターミナルで新規プロジェクト作成 & ディレクト移動

# cargo new (プロジェクト名)
% cargo new rust_todo_app
% cd rust_todo_app

②コンパイルして、コードを実行(Hello, world!)

% cargo run
   Compiling hello_rust v0.1.0 (/Users/user/works/rust_todo_app)
    Finished dev [unoptimized + debuginfo] target(s) in 2.80s
     Running `target/debug/rust_todo_app`
Hello, world!

サーバーの基本的なセットアップ

次に、axumでサーバーを起動させる機能だけを作っていきます。

(ここで出来上がるコード↓)
https://github.com/ask-nugey/rust_todo_app/commit/7fbbe80cd8bf476a072e1a99fa746c461e18c715

チャッピーと相談して出来上がったコードがこちら↓

  • src/main.rs
use axum::Router;

#[tokio::main]
async fn main() {
    // ①アプリケーションハンドラーの作成
    let app = Router::new();

    // ②127.0.0.1:3000でTCPリスナー(ソケット)をバインド(=接続の受け取り先を設定)
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    let addr = listener.local_addr().unwrap();

    println!("🤙 listening on {}", addr);

    // ③axumサーバーを起動
    axum::serve(listener, app).await.unwrap();
}

サーバーを起動を起動するには③でaxum::serve関数()に必要なものを引数に突っ込んで起動しているいるんだなってわかります。

この状態(コード)でコードを実行すると以下のようになります↓

% cargo run
   Compiling rust_todo_app v0.1.0 (/Users/nugey/work/rust_todo_app)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.97s
     Running `target/debug/rust_todo_app`
🤙 listening on 127.0.0.1:3000

けど、②let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")が全くよくわかりません。

ここについて深掘りしていきます。

TCPリスナーをバインドってなんぞ??

TCPリスナーがよくわからなかったのでチャッピーに詰問しました。
まとめると以下のような感じっぽい↓

  • TCPリスナーとは:
    • サーバーがクライアントからの接続要求を受け入れるための機能
    • 今回でいえば、サーバーが127.0.0.1:3000に来る接続を待機し続けるっぽい
      • この状態をリッスン(listen)というっぽい
クライアント            サーバー
-----------------------------------------------------
接続要求        ->     [TCPリスナー (listen)]
                                  
接続確立        <-     [TCPリスナー (accept)]
                         |
                      新しいソケット
-----------------------------------------------------
データの送受信   <->    データの送受信

電話で例えると、
- 電話番号が「127.0.0.1:3000」
- 電話機に電話番号を登録する(=バインドする)
- 電話をかけられる状態にしておく(=リッスン)
- 電話がかかったら繋がって話せる(サーバーとクライアントが送受信する)

電話の比喩                   TCPリスナーの機能
--------------------------------------------------------
電話をかけられる状態にする  ->  サーバーが特定のポートでリスニングする
電話が鳴る               ->  クライアントが接続要求を送る
電話を取る               ->  サーバーが接続要求を受け入れ、通信を開始する

つまり、let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")というコードは、TCPリスナーの設定情報(オブジェクト?)をlistener変数に詰め込んでいるという感じでしょうか。

電話の比喩でいうと、電話機はソケットになるのかな?
正確性は一旦置いておいて、今回は概要がわかれば良いことにしました。

TCPリスナーの詳細については別記事でまとめていくかも?

一旦、ここまで

axumでサーバーを起動しましたが、次回の記事ではTODOリストを表示するAPIを作成 します💪

https://zenn.dev/ask_nugey/articles/9b77e1d4db52c7


ヌギーのSNS(連絡先など)

Discussion