Dockerではじめるactix-web(Rust)とReact(TypeScript)

# はじめに

インフラはDockerとDocker Composeです。
バックエンドはactix-web(Rustのフレームワーク)
フロントエンドはReact(TypeScriptのフレームワーク)
データベースはMySqlです。

リマインダー(todoリスト)を目標に開発を行います。


レポジトリーはこちらです。

https://github.com/sugiyama404/practice_rust

RustはCargo.tomlに記述してパッケージをダウンロードする方式です。
パッケージの最新バージョンは以下で確認すると良いです。

https://crates.io

また、Rustはtargetレポジトリーが非常に重いので、gitignoreに入れておくといいです。


# バックエンド側

バックエンド側を実装します。


## 環境構築


```dockerfile
FROM rust:1-slim-buster

WORKDIR /usr/src/myapp
```

```docker-compose.yml
version: "3"
services:
  web:
    build: ./opt
    container_name: "practice-rust"
    tty: true
    volumes:
      - ./opt:/usr/src/myapp
```


```bash
docker compose exec web bash
cargo new practice-rust
```

以下のようなファイル構成になっていると思います。
.gitフォルダーが自動で作られます。



```text:ファイル構成
├── docker-compose.yml
└── opt
    ├── Dockerfile
    └── practice-rust
        ├── Cargo.toml
        └── src
            └── main.rs

```

```rust:main.rs
async fn main() {
    println!("Hello, world!");
}
```


## Hello, world!

```console
root@500969f0546b:/usr/src/myapp/practice-rust# cargo run
   Compiling practice-rust v0.1.0 (/usr/src/myapp/practice-rust)
    Finished dev [unoptimized + debuginfo] target(s) in 8.37s
     Running `target/debug/practice-rust`
Hello, world!
```

コンパイルに時間がかかります。

## actix-webを使う

actix-webはRustのwebのフレームワークです。

https://github.com/actix/actix-web

Cargo.tomlにactix-web="4"を追加します。

```text:Cargo.toml
[package]
name = "practice-rust"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web="4"
```

main.rsを以下のように書き換えます。


```rust:main.rs
use actix_web::{get, App, HttpServer, Responder};


#[get("/")]
async fn index() -> impl Responder {
    "Hello world!"
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new().service(index)
    })
    .bind(("web", 8080))?
    .run()
    .await
}
```

service(index)を追加することで、ルーティングしたい関数を追加します。


bind(("web", 8080))のwebはdocker-compseに書いたサービス名です。

## Hello world!する

![Screenshot 2023-01-27 at 17.09.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/acfe89aa-afca-c879-9fc6-72aa54c5b6ed.png)

# フロントエンド側

## React(typescript)を実装

```dockerfile
FROM rust:1-slim-buster

WORKDIR /usr/src/myapp
```

```docker-compose.yml
version: "3"
services:
  web:
    build: ./opt
    container_name: "practice-rust"
    working_dir: "/usr/src/myapp/practice-rust"
    tty: true
    volumes:
      - ./opt:/usr/src/myapp
    ports:
      - "8080:8080"
  frontend:
    build: ./frontend
    container_name: "frontend-rust"
    volumes:
      - ./frontend:/usr/src/app
    ports:
      - "3000:3000"
    tty: true
```
## viteでインストール

以下の順番でインストールを実行します。

```console
docker compose exec web bash
npm create vite@latest
cd rust-front
npm install
```
## wellcome画面

docker上では、そのまま開発できないので、設定ファイルをいじります。


```json:package.json
  "scripts": {
    "dev": "vite --host",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
```

```text:vite.config.ts
export default defineConfig({
  server: {
    host: '0.0.0.0',
    port: 3000,
  },
  plugins: [react()],
})
```

準備が完了したので、

```console
npm run dev
```

![Screenshot 2023-01-28 at 10.50.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/a870d49e-cf3d-283b-eb6d-3451cbe730aa.png)

## api通信

```rust:main.rs
use actix_web::{get, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Todo {
    id: i32,
    content: String,
    checked: bool,
}

#[get("/api/todo")]
async fn todo_index() -> impl Responder {
    HttpResponse::Ok().json(Todo {
        id: 1,
        content: "やることはapi".to_string(),
        checked: false,
    })
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new().service(todo_index)
    })
    .bind(("web", 8080))?
    .run()
    .await
}
```

```console
npm install axios
```

```typescript:app.tsx
import { useEffect, useState } from 'react'
import './App.css'
import axios from 'axios';

function App() {
  const url = "http://localhost/api/todo";
  const [count, setCount] = useState(0)

  useEffect(() => {
    axios.get(url).then((res) => {
      console.log(res.data);
    }).catch(error => {
      console.log(error);
    });
  }, [])
  return (
    <div className="App">
      helloworld
    </div>
  )
}

export default App
```

### nginxの導入

cors対策の為、nginxを導入しリバースプロキシを行います。
docker-compose.ymlに以下の行を追加します。

```dockerfile:docker-compose.yml
  webserver:
    build: ./webserver
    ports:
      - "80:80"
    volumes:
      - ./webserver:/etc/nginx/conf.d
```

rust.confを/webserverにいれ、リバースプロキシをします。

```conf:rust.conf
server {
    listen 80;
    listen [::]:80;
    server_name localhost;

    location / {
        proxy_pass http://frontend:3000;
    }

    location /api/ {
        proxy_pass http://web:8080;
    }

}
```

loccalhostにアクセスします。

![Screenshot 2023-01-28 at 19.49.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/d9faef45-9c0d-083b-f6ef-6b2951f7965f.png)

# バックエンド側

## mysqlに接続

dockerのmysql環境構築は割愛します。

以下の記事は、下記を参考にしています。

https://qiita.com/c0ridrew/items/42bcf381766ba224eed7



Cargo.tomlに以下を追加します。

```text:Cargo.toml
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
diesel = { version = "2.0", features = ["mysql","r2d2"] }
dotenv = "0.15"
r2d2 = "0.8"
```

```console
cargo install diesel_cli --no-default-features --features mysql --force
```

### migrationする

```console
diesel setup
diesel migration generate create_todos
```

migrationsディレクトリー配下にup.sqlとdown.sqlができていると思います。

```sql:up.sql
CREATE TABLE todos
(
    id INT PRIMARY KEY AUTO_INCREMENT,
    content TEXT DEFAULT NULL
);
```

```sql:down.sql
DROP TABLE todos;
```

mysqlのURLをenvファイルに書き込みます。

```config:.env
DATABASE_URL=mysql://rust:rust@db:3306/todoproject
```

tableを生成するには、以下のコマンドを実行します。

```console
diesel migration run
```

```rust:db.rs
use diesel::mysql::MysqlConnection;
use diesel::r2d2::ConnectionManager;
use dotenv::dotenv;

pub type Pool = r2d2::Pool<ConnectionManager<MysqlConnection>>;

pub fn establish_connection() -> Pool {
    dotenv().ok();

    std::env::set_var("RUST_LOG", "actix_web=debug");
    let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");

    // create db connection pool
    let manager = ConnectionManager::<MysqlConnection>::new(database_url);
    let pool: Pool = r2d2::Pool::builder()
        .build(manager)
        .expect("Failed to create pool.");
    pool
}
```

```rust:model.rs
use crate::schema::todos;
use serde::{Deserialize, Serialize};

#[derive(Queryable, Insertable, Deserialize, Serialize)]
#[diesel(table_name = todos)]
pub struct Todo {
    pub id: i32,
    pub content: String,
}

#[derive(Queryable, Insertable, Deserialize, Serialize)]
#[diesel(table_name = todos)]
pub struct Updatetodo {
    pub content: String,
}
```

```rust:schema.rs
// @generated automatically by Diesel CLI.

table! {
    todos (id) {
        id -> Integer,
        content -> Text,
    }
}
```

以下を実行すると、get,post,deleteができます。

```rust:main.rs
#[macro_use]
extern crate diesel;
use diesel::RunQueryDsl;
use actix_web::web::Data;
use actix_web::{get, post, delete, web, App, HttpResponse, HttpServer, Responder};
mod db;
mod model;
mod schema;

#[get("/api/todo")]
async fn todo_index(db: web::Data<db::Pool>) -> impl Responder {
    let mut conn = db.get().unwrap();
    let todo = schema::todos::table
        .load::<model::Todo>(&mut conn)
        .expect("Error not showing todo list");
    HttpResponse::Ok().json(todo)
}

#[post("/api/todo")]
async fn new_todo(db: web::Data<db::Pool>, c: web::Json<model::Updatetodo>) -> impl Responder {
    let mut conn = db.get().unwrap();
    let new_todo = model::Updatetodo {
        content: c.content.to_string(),
    };
    diesel::insert_into(schema::todos::dsl::todos)
        .values(&new_todo)
        .execute(&mut conn)
        .expect("Error not saving new todo");

    HttpResponse::Created().body("get ok")
}

#[delete("/api/todo/{id}")]
async fn delete_todo(db: web::Data<db::Pool>, path: web::Path<i32>) -> impl Responder {
    let id = path.into_inner();
    let mut conn = db.get().unwrap();
    let target = schema::todos::dsl::todos
                    .filter(schema::todos::dsl::id.eq(id));

    diesel::delete(target)
        .execute(&mut conn)
        .expect("Error deleting new post");

    HttpResponse::Created().body("Delete complete")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {

    let pool = db::establish_connection();
    HttpServer::new(move|| {
        App::new()
        .app_data(Data::new(pool.clone())
        .service(todo_index)
        .service(new_todo)
        .service(delete_todo)
    })
    .bind(("web", 8080))?
    .run()
    .await
}

```

# フロントエンド側

## reactにリマインダーを実装

typesriptは型宣言が必要です。
でも、Rustより遥かに簡単。

```tsx:app.tsx
  type Todo = {
    id: number,
    content: string,
  }
```

useState<型>(初期値)です。
setTodosはセッターです。

```tsx:app.tsx
const [todos, setTodos] = useState<Todo[]>([]);
```

stringを型にしたパターンです。

```tsx:app.tsx
useState<string>("");
```

```tsx:app.tsx
import { useEffect, useState } from 'react'
import './App.css'
import axios from 'axios';

function App() {
  const url = "http://localhost/api/todo";
  const [inputValue, setInputValue] = useState("");
  const [todos, setTodos] = useState<Todo[]>([]);

  type Todo = {
    id: number,
    content: string,
  }

  const onWriting = (e: React.ChangeEvent<HTMLInputElement>) => {
    e.preventDefault();
    setInputValue(e.target.value);
  };

  const onSave = (e: { preventDefault: () => void }) => {
    e.preventDefault();
    if (!inputValue) { return; }

    axios.post('/api/todo', { content: inputValue })
      .then(function (response) {
        console.log(response.data);
        axios.get(url).then((res) => {
          setTodos([...res.data]);
        }).catch(error => {
          console.log(error);
        });
      })

    setInputValue("");
  };

  const onDelete = (uid: number) => {

    axios.delete('/api/todo/' + String(uid))
      .then(function (response) {
        console.log(response.data);
        axios.get(url).then((res) => {
          setTodos([...res.data]);
        }).catch(error => {
          console.log(error);
        });
      })
  };

  useEffect(() => {
    axios.get(url).then((res) => {
      setTodos([...res.data]);
    }).catch(error => {
      console.log(error);
    });
  }, [])
  return (
    <div className="App">
      <h1>リマインダー</h1>
      <form onSubmit={(e) => onSave(e)}>
        <input type="text" style={{ width: "400px", height: "30px" }} value={inputValue} onChange={(e) => onWriting(e)} />
        <input type="submit" style={{ width: "100px", height: "35px" }} value="送信" />
      </form>
      <br></br>
      <table border={1}>
        {todos.map((arr: Todo) => (
          <tr>
            <td style={{ textAlign: 'left' }} width={500}>{arr.content}</td>
            <td>
              <button onClick={() => onDelete(arr.id)}>削除</button>
            </td>
          </tr>
        ))}
      </table>
    </div >
  )
}

export default App

```

# 結果

![rust_movies.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/0f0b369b-a923-0fe6-f06f-9d0f1271ea44.gif)