Chuyển đến nội dung chính

Bài 10: Axum Framework & REST API

Axum setup, routing, handlers, extractors. State management, middleware (tower). Serde serialization, JSON responses. Error handling, custom errors. So sánh Axum vs Actix-web vs Rocket.

💻 Lập trình — Bài 10 Bài 10: Axum Framework & REST API

Rust: Từ Cơ bản đến Nâng cao

Phần 3: Async Rust & Web Development

xdev.asia

1. Axum Setup

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "trace"] }
tracing = "0.1"
tracing-subscriber = "0.3"
use axum::{routing::{get, post}, Router, Json, extract::{State, Path, Query}};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;

#[derive(Clone)]
struct AppState {
    db: Arc<RwLock<Vec<Product>>>,
}

#[tokio::main]
async fn main() {
    tracing_subscriber::init();

    let state = AppState {
        db: Arc::new(RwLock::new(Vec::new())),
    };

    let app = Router::new()
        .route("/products", get(list_products).post(create_product))
        .route("/products/{id}", get(get_product).put(update_product).delete(delete_product))
        .with_state(state);

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

2. Handlers & Extractors

#[derive(Serialize, Deserialize, Clone)]
struct Product {
    id: String,
    name: String,
    price: f64,
}

#[derive(Deserialize)]
struct CreateProduct {
    name: String,
    price: f64,
}

#[derive(Deserialize)]
struct Pagination {
    page: Option<u32>,
    limit: Option<u32>,
}

async fn list_products(
    State(state): State<AppState>,
    Query(pagination): Query<Pagination>,
) -> Json<Vec<Product>> {
    let db = state.db.read().await;
    let page = pagination.page.unwrap_or(1);
    let limit = pagination.limit.unwrap_or(20);
    let skip = ((page - 1) * limit) as usize;
    let products: Vec<Product> = db.iter().skip(skip).take(limit as usize).cloned().collect();
    Json(products)
}

async fn create_product(
    State(state): State<AppState>,
    Json(input): Json<CreateProduct>,
) -> (axum::http::StatusCode, Json<Product>) {
    let product = Product {
        id: uuid::Uuid::new_v4().to_string(),
        name: input.name,
        price: input.price,
    };
    state.db.write().await.push(product.clone());
    (axum::http::StatusCode::CREATED, Json(product))
}

async fn get_product(
    State(state): State<AppState>,
    Path(id): Path<String>,
) -> Result<Json<Product>, AppError> {
    let db = state.db.read().await;
    let product = db.iter().find(|p| p.id == id)
        .cloned()
        .ok_or(AppError::NotFound(format!("Product {}", id)))?;
    Ok(Json(product))
}

3. Error Handling

use axum::response::IntoResponse;
use axum::http::StatusCode;

enum AppError {
    NotFound(String),
    BadRequest(String),
    Internal(anyhow::Error),
}

impl IntoResponse for AppError {
    fn into_response(self) -> axum::response::Response {
        let (status, message) = match self {
            AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
            AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
            AppError::Internal(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
        };
        (status, Json(serde_json::json!({ "error": message }))).into_response()
    }
}

4. Middleware với Tower

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

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

Bài tiếp theo: SQLx & Database Integration.