ข้ามไปยังเนื้อหา

เริ่มต้น Backend

Cargo workspace ของ Rust จริง ๆ ภายใน taskflow/backend/ โดยมี member crate ตัวเดียวคือ api (ชื่อ package taskflow-api) ที่จะเติบโตกลายเป็น Axum server เต็มรูปแบบในโมดูลถัดไป เมื่อจบบทเรียนนี้ cargo run จะพิมพ์ TaskFlow API ออกทาง terminal — เป็นหลักฐานว่า workspace, crate และทุก dependency คอมไพล์เข้าด้วยกันได้ ก่อนที่เราจะเขียน handler สักตัว

backend/
├── Cargo.toml # workspace root
└── api/
├── Cargo.toml # crate taskflow-api
└── src/
└── main.rs

Cargo workspace ให้หลาย crate ใช้ Cargo.lock และไดเรกทอรี build target/ ร่วมกันได้ วันนี้ TaskFlow มี crate เดียว (api) แต่การจัดโครงสร้างเป็น workspace ตั้งแต่แรกหมายความว่าการเพิ่ม crate ตัวที่สองในภายหลัง — เช่น library taskflow-core ที่รวม domain type ที่ใช้ร่วมกัน หรือเครื่องมือ CLI สำหรับ admin — จะเป็นแค่การเพิ่มบรรทัดเดียวใน members โดยไม่ต้องปรับโครงสร้างใหม่และไม่ต้องมี dependency เวอร์ชันซ้ำซ้อน

ข้อดี

  • Cargo.lock ไฟล์เดียวสำหรับทั้ง backend — ทุก crate ใน workspace resolve dependency ไปที่เวอร์ชันเดียวกันเป๊ะ ๆ หลีกเลี่ยงบั๊กจาก version skew
  • ไดเรกทอรี target/ ที่ใช้ร่วมกัน ทำให้ crate ที่พึ่งพากันไม่ต้องคอมไพล์ dependency ที่ใช้ร่วมกันซ้ำสองครั้ง
  • ขยายได้ไม่ยาก: วันนี้มี crate เดียว (api); พรุ่งนี้อาจเป็น api + core + binary สำหรับ background-worker ทั้งหมด build ได้ด้วย cargo build คำสั่งเดียว

ข้อเสีย

  • สำหรับโปรเจกต์ที่มี crate เดียว workspace เพิ่มพิธีกรรมเล็กน้อย (มี Cargo.toml เพิ่มอีกไฟล์) เมื่อเทียบกับการ cargo new เปล่า ๆ ที่ root ของ repo
  • path ที่อ้างอิงแบบ workspace-relative (members = ["api"]) หมายความว่า Cargo.toml ของแต่ละ member crate อยู่ลึกกว่าหนึ่งชั้นเมื่อเทียบกับการทำเป็น project เดี่ยว ๆ — ลืมง่ายเวลา copy-paste path

เรายอมรับ overhead เล็กน้อยนี้ตอนนี้ เพราะโมดูล testing ของ TaskFlow ในภายหลังจะเพิ่มการตั้งค่า integration test ที่ได้ประโยชน์จากโครงสร้าง workspace

จากไดเรกทอรี taskflow/backend/:

Terminal window
cd taskflow/backend
cargo new api

คำสั่งนี้สร้าง api/Cargo.toml และ api/src/main.rs พร้อมโครง “Hello, world!” มาตรฐานของ Cargo ต่อไปเราจะเปลี่ยน api ให้เป็น workspace member และใส่ dependency จริงเข้าไป

สร้าง taskflow/backend/Cargo.toml (ไฟล์ ใหม่ แยกจาก api/Cargo.toml):

[workspace]
members = ["api"]
resolver = "2"

members ระบุ crate ทุกตัวที่เป็นสมาชิกของ workspace นี้ — ตอนนี้มีแค่ api resolver = "2" คือการเลือกใช้ feature resolver รุ่นใหม่ของ Cargo ที่เป็นค่า default สำหรับ workspace ใหม่ และช่วยหลีกเลี่ยงปัญหาการรวม feature ข้าม crate ที่ไม่คาดคิด

แทนที่ api/Cargo.toml ที่ถูก generate ด้วยเนื้อหานี้:

[package]
name = "taskflow-api"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = { version = "0.7", features = ["ws", "macros"] }
tokio = { version = "1", features = ["full"] }
tower-http = { version = "0.6", features = ["cors", "trace"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "chrono", "macros"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
uuid = { version = "1", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
jsonwebtoken = "9"
argon2 = "0.5"
deadpool-redis = "0.16"
redis = { version = "0.27", features = ["tokio-comp"] }
thiserror = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

สังเกตว่า name ของ package คือ taskflow-api แม้ว่าไดเรกทอรีจะชื่อ api — ชื่อ crate กับชื่อไดเรกทอรีไม่จำเป็นต้องตรงกัน และการใส่ prefix taskflow- ให้ชื่อ crate ที่จะ publish ช่วยหลีกเลี่ยงการชนกับ crate api ตัวอื่นบน crates.io

dependency แต่ละตัวมีไว้ทำอะไร:

  • axum (ws, macros) — web framework ที่จะ route ทุก HTTP request; ws เปิดใช้ WebSocket upgrade ที่โมดูล Realtime จะใช้ macros เปิดใช้ macro อย่าง #[debug_handler] ที่ช่วยด้าน ergonomics ของ handler
  • tokio (full) — async runtime ที่ทุกอย่างรันอยู่บนนั้น; full ดึง TCP listener, task scheduler, timer และ sync primitive ที่เราต้องใช้ตลอดทุกโมดูลเข้ามาด้วย
  • tower-http (cors, trace) — middleware layer: cors ให้ browser ที่ FRONTEND_ORIGIN เรียก API ได้ trace log ทุก request/response ผ่าน tracing
  • sqlx (runtime-tokio, postgres, uuid, chrono, macros) — driver PostgreSQL แบบ async ที่ตรวจ query ตอน compile-time; uuid และ chrono ให้ SQLx map คอลัมน์ uuid และ timestamp ของ Postgres ไปเป็น Rust type ได้ตรง ๆ macros เปิดใช้ query!/query_as!
  • serde (derive) — #[derive(Serialize, Deserialize)] สำหรับทุก struct ของ request/response และโมเดลฐานข้อมูล
  • serde_json — encode/decode JSON ที่ axum::Json ใช้ภายใน และตรงไหนก็ตามที่เราสร้าง JSON เอง
  • uuid (v4, serde) — สร้าง UUID แบบ v4 แบบสุ่ม ที่ใช้เป็น primary key ของ board, column และ card; ส่วน serde ทำให้ (de)serialize เป็น JSON ได้
  • chrono (serde) — type สำหรับวันที่/เวลา สำหรับคอลัมน์ created_at/updated_at พร้อม serde รองรับ timestamp ใน JSON
  • jsonwebtoken — encode และ verify JWT access/refresh token ที่ออกในโมดูล Authentication
  • argon2 — hash และ verify รหัสผ่านผู้ใช้; ห้ามเก็บหรือเปรียบเทียบรหัสผ่านแบบ plaintext เด็ดขาด
  • deadpool-redis — connection pool แบบ async สำหรับ Redis เพื่อให้ handler ยืม connection แทนที่จะเปิดใหม่ทุกครั้งที่มี request
  • redis (tokio-comp) — Redis client แบบ async ที่อยู่เบื้องหลัง deadpool-redis; ส่วน tokio-comp ทำให้เข้ากันได้กับ Tokio runtime ของเรา
  • thiserror — derive std::error::Error และ Display ให้ enum error ของ domain เรา ซึ่งภายหลังเราจะแปลงเป็น HTTP response
  • tracing — logging/instrumentation แบบมีโครงสร้างและมีระดับ (level) ครอบคลุมทั้ง server
  • tracing-subscriber (env-filter) — ต่อสาย output ของ tracing; env-filter ให้เราควบคุมความละเอียดของ log ผ่านตัวแปร environment RUST_LOG

แทนที่ api/src/main.rs ด้วยจุดเริ่มต้นแบบขั้นต่ำ — ยังไม่มี server เป็นแค่หลักฐานว่า crate build ได้และ link ทุก dependency ได้:

fn main() {
println!("TaskFlow API");
}

โค้ดนี้ตั้งใจให้ยังไม่ทำอะไรมากไปกว่านี้ โมดูล Backend Foundations (โมดูล 3) จะแทนที่ด้วยการตั้งค่า Axum server จริง — router, state และ axum::serve

จาก taskflow/backend/ build และรัน crate api:

Terminal window
cargo run -p api

การรันครั้งแรกจะคอมไพล์ทุก dependency ในรายการข้างบน ซึ่งอาจใช้เวลาหนึ่งถึงสองนาที ผลลัพธ์ที่คาดหวังหลัง build เสร็จ:

TaskFlow API

ถ้า compile ผ่านและพิมพ์บรรทัดนั้นออกมา แปลว่า workspace ต่อสายถูกต้องและ dependency ทุกเวอร์ชัน resolve ได้ ยืนยันเพิ่มด้วยว่าการเช็กทั้ง workspace ผ่านสะอาด:

Terminal window
cargo check --workspace

ควร exit โดยไม่มี error (คำเตือนเรื่อง dependency ที่ยังไม่ได้ใช้เป็นเรื่องปกติในขั้นนี้ — เรายังไม่ได้ใช้ dependency ส่วนใหญ่)

คุณสร้าง Cargo workspace ที่ taskflow/backend/ แล้ว โดยมี member ตัวเดียวคือ crate taskflow-api ใน api/ Cargo.toml ที่ root ประกาศ members = ["api"]; api/Cargo.toml ล็อก dependency ครบชุดที่ TaskFlow ต้องการ — Axum, Tokio, SQLx, Serde, JWT, Argon2, Redis และ tracing — แต่ละตัวมีจุดประสงค์ที่คุณเข้าใจแล้ว main.rs แบบขั้นต่ำพิสูจน์ว่าทุกอย่าง compile และรันได้ด้วย cargo run -p api ต่อไปเราจะสร้างโครง frontend ใน frontend-init