Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Axum พื้นฐาน

Axum เป็น web framework จาก Tokio team ออกแบบมาสำหรับ async Rust

Setup

# Cargo.toml
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Hello World

use axum::{routing::get, Router};

#[tokio::main]
async fn main() {
    // สร้าง router
    let app = Router::new()
        .route("/", get(hello));

    // สร้าง TCP listener
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();

    println!("🚀 Server running at http://localhost:3000");

    // รัน server
    axum::serve(listener, app).await.unwrap();
}

async fn hello() -> &'static str {
    "Hello, World!"
}

รัน:

cargo run
```text

เปิดเบราว์เซอร์ไปที่ http://localhost:3000

---

## Routes

### Basic Routes

```rust,ignore
let app = Router::new()
    .route("/", get(index))
    .route("/about", get(about))
    .route("/contact", get(contact));

HTTP Methods

use axum::routing::{get, post, put, delete};

let app = Router::new()
    .route("/users", get(list_users).post(create_user))
    .route("/users/:id", get(get_user).put(update_user).delete(delete_user));

Nested Routes

let api_routes = Router::new()
    .route("/users", get(list_users))
    .route("/posts", get(list_posts));

let app = Router::new()
    .nest("/api/v1", api_routes)
    .route("/", get(index));

// ผลลัพธ์:
// GET /api/v1/users
// GET /api/v1/posts
// GET /

Handler Functions

Basic Handler

async fn hello() -> &'static str {
    "Hello, World!"
}

async fn json_response() -> axum::Json<serde_json::Value> {
    axum::Json(serde_json::json!({
        "message": "Hello JSON"
    }))
}

Return Types

TypeDescription
&'static strPlain text
StringDynamic text
Json\<T\>JSON response
Html\<String\>HTML response
(StatusCode, T)Custom status
Result\<T, E\>With error handling
use axum::{response::Html, http::StatusCode};

async fn html_page() -> Html<String> {
    Html("<h1>Hello HTML</h1>".to_string())
}

async fn custom_status() -> (StatusCode, &'static str) {
    (StatusCode::CREATED, "Created!")
}

Extractors

ดึงข้อมูลจาก request:

Path Parameters

use axum::extract::Path;

// GET /users/123
async fn get_user(Path(id): Path<u32>) -> String {
    format!("User ID: {}", id)
}

// GET /users/123/posts/456
async fn get_user_post(Path((user_id, post_id)): Path<(u32, u32)>) -> String {
    format!("User {} Post {}", user_id, post_id)
}

Query Parameters

use axum::extract::Query;
use serde::Deserialize;

#[derive(Deserialize)]
struct SearchParams {
    q: String,
    page: Option<u32>,
}

// GET /search?q=rust&page=2
async fn search(Query(params): Query<SearchParams>) -> String {
    format!("Searching: '{}' page {:?}", params.q, params.page)
}

JSON Body

use axum::extract::Json;
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct CreateUser {
    name: String,
    email: String,
}

#[derive(Serialize)]
struct User {
    id: u32,
    name: String,
    email: String,
}

// POST /users with JSON body
async fn create_user(Json(payload): Json<CreateUser>) -> Json<User> {
    let user = User {
        id: 1,
        name: payload.name,
        email: payload.email,
    };
    Json(user)
}

Headers

use axum::http::HeaderMap;

async fn read_headers(headers: HeaderMap) -> String {
    if let Some(auth) = headers.get("authorization") {
        format!("Auth: {:?}", auth)
    } else {
        "No auth header".to_string()
    }
}

State

แชร์ข้อมูลระหว่าง handlers:

use axum::extract::State;
use std::sync::Arc;

struct AppState {
    db_pool: String,  // ปกติใช้ connection pool จริง
}

#[tokio::main]
async fn main() {
    let state = Arc::new(AppState {
        db_pool: "postgres://...".to_string(),
    });

    let app = Router::new()
        .route("/", get(handler))
        .with_state(state);

    // ...
}

async fn handler(State(state): State<Arc<AppState>>) -> String {
    format!("DB: {}", state.db_pool)
}

Error Handling

use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
};

enum AppError {
    NotFound,
    InternalError(String),
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            AppError::NotFound => (StatusCode::NOT_FOUND, "Not found"),
            AppError::InternalError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.leak()),
        };
        (status, message).into_response()
    }
}

async fn fallible_handler() -> Result<String, AppError> {
    // อาจ return Err(AppError::NotFound)
    Ok("Success!".to_string())
}

Middleware

use axum::middleware;
use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer;

let app = Router::new()
    .route("/", get(handler))
    .layer(CorsLayer::permissive())
    .layer(TraceLayer::new_for_http());

ลองทำดู! 🎯

  1. สร้าง API ที่มี GET และ POST
  2. ใช้ Path extractor รับ id
  3. ใช้ Json extractor รับ body

🌍 Complete CRUD API Example

ตัวอย่าง Todo API แบบครบ:

use axum::{
    extract::{Path, State},
    http::StatusCode,
    routing::{get, post, put, delete},
    Json, Router,
};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, RwLock};

// Data models
#[derive(Clone, Serialize, Deserialize)]
struct Todo {
    id: u32,
    title: String,
    completed: bool,
}

#[derive(Deserialize)]
struct CreateTodo {
    title: String,
}

type Db = Arc<RwLock<Vec<Todo>>>;

// Handlers
async fn list_todos(State(db): State<Db>) -> Json<Vec<Todo>> {
    let todos = db.read().unwrap().clone();
    Json(todos)
}

async fn create_todo(
    State(db): State<Db>,
    Json(input): Json<CreateTodo>,
) -> (StatusCode, Json<Todo>) {
    let mut todos = db.write().unwrap();
    let id = todos.len() as u32 + 1;
    let todo = Todo {
        id,
        title: input.title,
        completed: false,
    };
    todos.push(todo.clone());
    (StatusCode::CREATED, Json(todo))
}

async fn get_todo(
    State(db): State<Db>,
    Path(id): Path<u32>,
) -> Result<Json<Todo>, StatusCode> {
    let todos = db.read().unwrap();
    todos
        .iter()
        .find(|t| t.id == id)
        .cloned()
        .map(Json)
        .ok_or(StatusCode::NOT_FOUND)
}

async fn delete_todo(
    State(db): State<Db>,
    Path(id): Path<u32>,
) -> StatusCode {
    let mut todos = db.write().unwrap();
    if let Some(pos) = todos.iter().position(|t| t.id == id) {
        todos.remove(pos);
        StatusCode::NO_CONTENT
    } else {
        StatusCode::NOT_FOUND
    }
}

#[tokio::main]
async fn main() {
    let db: Db = Arc::new(RwLock::new(vec![]));

    let app = Router::new()
        .route("/todos", get(list_todos).post(create_todo))
        .route("/todos/:id", get(get_todo).delete(delete_todo))
        .with_state(db);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();
    println!("🚀 Server running at http://localhost:3000");
    axum::serve(listener, app).await.unwrap();
}

📋 API Endpoints

MethodEndpointDescription
GET/todosList all todos
POST/todosCreate new todo
GET/todos/:idGet todo by ID
DELETE/todos/:idDelete todo

🚀 Deployment Checklist

+-------------------------------------------------------------------+
|                    Production Deployment                          |
+-------------------------------------------------------------------+
|                                                                   |
|   1. Build Release                                                |
|      cargo build --release                                        |
|                                                                   |
|   2. Dockerfile                                                   |
|      FROM rust:1.75 as builder                                    |
|      WORKDIR /app                                                 |
|      COPY . .                                                     |
|      RUN cargo build --release                                    |
|                                                                   |
|      FROM debian:bookworm-slim                                    |
|      COPY --from=builder /app/target/release/myapp /usr/local/bin |
|      CMD ["myapp"]                                                |
|                                                                   |
|   3. Environment Variables                                        |
|      DATABASE_URL=postgres://...                                  |
|      PORT=3000                                                    |
|                                                                   |
|   4. Deploy Options                                               |
|      * Docker -> AWS ECS / Google Cloud Run                       |
|      * Binary -> VPS (DigitalOcean, Linode)                       |
|      * Serverless -> AWS Lambda + cargo-lambda                    |
|                                                                   |
+-------------------------------------------------------------------+

สรุป

ConceptExample
Route.route("/path", get(handler))
PathPath(id): Path<u32>
QueryQuery(params): Query<T>
JSONJson(body): Json<T>
StateState(state): State<Arc<T>>
Nested.nest("/api", routes)

👉 ต่อไป: สร้าง REST API