Developer Logs

Simple REST API with Rust (Axum)

RUST
REST API

สร้าง REST API ง่าย ๆ ด้วย Rust (Axum)

Days: 18 | Publish : 11/05/2025

Simple REST API with Rust (Axum)
              

ช่วงนี้จะวน ๆ อยู่กับ Rust ค่อนข้างเยอะหน่อย ซึ่งโจทย์ในวันนี้ก็จะเป็นทดลองสร้าง REST API ขึ้นมา โดยใช้ Rust ในการสร้าง Service

Service ที่จะทำการสร้างในวันนี้จะเป็น REST API ในรูปแบบที่ไม่ได้ซับซ้อนอะไร เอาแค่เพื่อให้เข้าใจในการทำงาน ว่าโค้ดแต่ละส่วนเราจะ implement ยังไงได้บ้าง

Diagram ของสิ่งที่เราจะสร้างกันในวันนี้ RUST REST API diagram

จะเป็น Service โดยคอนเซปก็ประมาณเก็บข้อมูล Stock โดยเราจะเริ่มต้นกันแค่ GET กับ POST กันก่อน


Project setup

ก่อนจะไปกันต่อ สิ่งที่เราต้องเตรียมให้พร้อมสำหรับการทำงานทั้งหมดนี้ก็จะเป็น

- Rust แน่ ๆ ล่ะ จะขาดสิ่งนี้ไปได้ยังไงครับบบบ 5555 - ซึ่งถ้าใครยังไม่ได้ install กัน ก็ไปตามกันได้ที่ official doc ได้เลย - https://www.rust-lang.org/tools/install - MongoDB - Postman สำหรับการทดสอบ API endpoint

Rust dependencies

ในครั้งนี้ผมเลือก Axum ในการสร้าง Web service https://docs.rs/axum/latest/axum/ ซึ่งก็ยังมีตัวเลือกอื่น ๆ ที่ยังมีอยู่อีก เช่น Actix, Rocket ส่วนนี้ก็แล้วแต่เลือกได้เลยนะครับ

Cargo.toml


  [package]
  name = "inventory-service"
  version = "0.1.0"
  edition = "2021"

  [dependencies]
  axum = "0.8.4"
  tokio = { version = "1", features = ["full"] }
  serde = { version = "1", features = ["derive"] }
  serde_json = "1"
  tower = "0.5.2"
  dotenv = "0.15.0"
  mongodb = "3.2.3"
  futures = "0.3.31"
  tower-http = { version = "0.3", features = ["trace"] }
  bson = "2.14.0"


เริ่ม

ออกสตาร์ทกันง่ายที่สุด ด้วยการเริ่มจากการสร้าง fn main() ขึ้นมาก่อน ซึ่งสิ่งที่เราจะทำกันก่อน ก็จะเป็น database connector

Database connect


  use dotenv::dotenv;
  use mongodb::{
      bson::doc,
      Client,
      Collection,
      bson::oid::ObjectId,
  };

  .
  .
  .
  #[tokio::main]
  async fn main() {
      dotenv().ok();

      let mongo_uri = env::var("INVENTORY_DB_URL").expect("INVENTORY_DB_URL must be set");
      println!("MongoDB URI: {}", mongo_uri);
      let client = Client::with_uri_str(&mongo_uri)
          .await
          .expect("Failed to connect to MongoDB");
      let db = client.database("test_service");
  let collection = db.collection::<InventoryItem>("inventory");
  .
  .
  .
  }

เปิดง่าย ๆ กันก่อนกับการ Connect database โดยผมได้ใช้ .env file ในครั้งนี้ด้วย ซึ่ง INVENTORY_DB_URL ก็จะเป็นตัวแปรที่สร้างไว้ใน .env file เรียบร้อยแล้ว

เราก็จะ connect ไปที่ database และ collection ที่เราต้องการ ซึ่ง collection ในตัวอย่างนี้ คือ inventory

ซึ่งมาถึงตรงนี้ เราก็จะได้ database collection มาอยู่ในตัวแปร collection ในบรรทัดนี้ไปเรียบร้อยแล้ว let collection = db.collection::<InventoryItem>("inventory");


ทีนี้อาจจะสงสัยกันว่า แล้ว Structure ของ InventoryItem เราจะกำหนดได้ยังไงล่ะ ?


  .
  .
  .

  use mongodb::{
      bson::doc,
      Client,
      Collection,
      bson::oid::ObjectId,
  };

  #[derive(Debug, Serialize, Deserialize)]
  struct InventoryItem {
      #[serde(rename = "_id")]
      _id: ObjectId,
      name: String,
      quantity: i32,
  }

  .
  .
  .
  # fn main()
  .
      let collection = db.collection::<InventoryItem>("inventory");
      .
  .
  .

เราก็จะได้โครงสร้างที่จะใช้ใน collection


API routing

เมื่อ connect db ได้เรียบร้อยแล้ว ถัดมาเราจะมาเริ่มทำ routing เพื่อให้ API สามารถยิงทดสอบใน Postman ได้

  .
  .
  use axum::{
      extract::{Json, State},
      response::IntoResponse,
      routing::{get, post},
      Router,
  };
  .
  .
  # fn main()
  .
      let app = Router::new()
          .route("/inventory", get(check_stock))
          .route("/inventory/addItems", post(add_items))
          .with_state(state);

      let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
      axum::serve(listener, app).await.unwrap();
  .
  .
  .

ผมทำการ routing มา 2 จุด /inventory รับ method GET /inventory/addItems รับ method POST

หลังจากนั้นก็เปิด Listener มาที่ port :3000 ทีนี้อาจจะสงสัยว่า แล้ว check_stock กับ add_items จะเขียนยังไงต่อล่ะ ?

ไปกันต่อ

  .
  async fn check_stock() {
      println!("Checking stock...");
  }
  async fn add_items() {
      println!("Adding items...");
  }
  .

ก็เขียนเพิ่มขึ้นมาเป็นฟังก์ชันใหม่ แล้วเราก็จะพบปัญหาเพิ่มขึ้นมาว่า อ้าว … แล้วถ้าอยากจะส่งตัวแปรจากฟังก์ชัน main ไปที่ฟังก์ชันอื่น ??? เราจะทำยังไงได้บ้างล่ะ ???

Axum คิดเรื่องนี้มาให้เราเรียบร้อยแล้วครับ ถ้าผมต้องการส่งตัวแปร collection ที่ connect เรียบร้อยแล้วไปที่ function ย่อย Axum มีสิ่งที่เรียกว่า State อยู่ครับ! ที่จะใช้สำหรับการจัดการเรื่องนี้ ref: https://docs.rs/axum/latest/axum/#using-the-state-extractor


Handling params by State

  use std::{
      sync::Arc,
      env,
  };
  use axum::{
      extract::{Json, State},
      response::IntoResponse,
      routing::{get, post},
      Router,
  };
  use mongodb::{
      bson::doc,
      Client,
      Collection,
      bson::oid::ObjectId,
  };
  .
  .

  #[derive(Clone)]
  struct AppState {
      collection: Collection<InventoryItem>,
  }

  # fn main()
  .
      let collection = db.collection::<InventoryItem>("inventory");

      let state = Arc::new(AppState { collection });
      .
  .
  .

ผมทำการสร้าง AppState แล้วก็ส่งไปให้ fn อื่น ๆ ใช้ได้ ซึ่งสิ่งที่ผมส่งไปก็คือตัวแปร collection โดยจะส่งไปให้ fn อื่น ด้วยวิธีการ

  .
  .
  async fn check_stock(
      State(state): State<Arc<AppState>>
  ) {
  println!("Checking stock...");
  }

  async fn add_items(
      State(state): State<Arc<AppState>>
  ) {
  println!("Adding items...");
  }
  .
  .

Fetch data

ทีนี้มาเจาะใน แต่ละฟังก์ชันกันบ้าง ถ้าสำหรับการ Check_stock ผมก็จะใช้เป็นการ fetch ง่าย ๆ และต้องการให้ return เป็น Json ด้วย ผมก็จะเรียกใช้เป็นประมาณนี้


  async fn check_stock(
      State(state): State<Arc<AppState>>
  ) -> impl IntoResponse {
      println!("Checking stock...");
      let filter = doc! { "name": "Widget" };

      let mut cursor = state
          .collection
          .find(filter)
          .await
          .expect("Failed to execute find.");

      let mut items = Vec::new();
      while let Some(doc) = cursor
          .try_next()
          .await
          .expect("Error while iterating")
      {
          let item = InventoryItemResponse {
              _id: doc._id.to_hex(),
              name: doc.name,
              quantity: doc.quantity,
          };
          items.push(item);
      }

      Json(items)
  }


Add data


  async fn add_items(
      State(state): State<Arc<AppState>>,
      Json(payload): Json<InventoryItem>,
  ) -> impl IntoResponse {
      println!("Adding items...");

      state
          .collection
          .insert_one(&payload)
          .await
          .expect("Failed to insert document");

      Json(payload)
  }


Complete file

REST API ตัวอย่างในครั้งนี้ก็จะได้เป็นตามนี้ครับ


  use dotenv::dotenv;
  use futures::stream::TryStreamExt;
  use std::{
      sync::Arc,
      env,
  };
  use serde::{
      Deserialize,
      Serialize
  };
  use axum::{
      extract::{Json, State},
      response::IntoResponse,
      routing::{get, post},
      Router,
  };
  use mongodb::{
      bson::doc,
      Client,
      Collection,
      bson::oid::ObjectId,
  };

  #[derive(Debug, Serialize, Deserialize)]
  struct InventoryItem {
      #[serde(rename = "_id")]
      _id: ObjectId,
      name: String,
      quantity: i32,
  }

  #[derive(Debug, Serialize)]
  struct InventoryItemResponse {
      _id: String,
      name: String,
      quantity: i32,
  }

  #[derive(Clone)]
  struct AppState {
      collection: Collection<InventoryItem>,
  }

  #[tokio::main]
  async fn main() {
      dotenv().ok();

      let mongo_uri = env::var("INVENTORY_DB_URL").expect("INVENTORY_DB_URL must be set");
      println!("MongoDB URI: {}", mongo_uri);
      let client = Client::with_uri_str(&mongo_uri)
          .await
          .expect("Failed to connect to MongoDB");
      let db = client.database("test_service");
      println!("Connected to MongoDB database: {}", db.name());
      println!("MongoDB collections: {:?}", db.list_collection_names().await.unwrap());

      let collection = db.collection::<InventoryItem>("inventory");

      let state = Arc::new(AppState { collection });

      let app = Router::new()
          .route("/inventory", get(check_stock))
          .route("/inventory/addItems", post(add_items))
          .with_state(state);

      let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
      axum::serve(listener, app).await.unwrap();

  }


  async fn check_stock(
      State(state): State<Arc<AppState>>
  ) -> impl IntoResponse {
      println!("Checking stock...");
      let filter = doc! { "name": "Widget" };

      let mut cursor = state
          .collection
          .find(filter)
          .await
          .expect("Failed to execute find.");

      let mut items = Vec::new();
      while let Some(doc) = cursor
          .try_next()
          .await
          .expect("Error while iterating")
      {
          let item = InventoryItemResponse {
              _id: doc._id.to_hex(),
              name: doc.name,
              quantity: doc.quantity,
          };
          items.push(item);
      }

      Json(items)
  }

  async fn add_items(
      State(state): State<Arc<AppState>>,
      Json(payload): Json<InventoryItem>,
  ) -> impl IntoResponse {
      println!("Adding items...");

      state
          .collection
          .insert_one(&payload)
          .await
          .expect("Failed to insert document");

      Json(payload)
  }


ก็เป็นการ experiment กับการทดลองเขียน REST API ด้วย Rust กัน ให้พอเห็นภาพในการสร้าง Service โดยใช้ RUST + Axum

>_JV