正在加载,请稍候…

Rust Axum Web Framework: Build Fast, Safe REST APIs

Build production REST APIs with Rust's Axum framework — routing, extractors, state sharing, middleware, SQLx database access, error handling, and testing.

Why Axum for Web APIs

Axum combines Rust's safety guarantees with ergonomic web primitives. It's built on Tokio and Tower, giving you a composable, high-performance HTTP server.

Setup

# Cargo.toml
[dependencies]
axum = { version = "0.7", features = ["macros"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono"] }
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "trace"] }
tracing = "0.1"
tracing-subscriber = "0.3"

Basic Application

use axum::{
    routing::{get, post},
    Router,
    Json,
    extract::{Path, State},
    http::StatusCode,
};
use std::sync::Arc;

#[tokio::main]
async fn main() {
    tracing_subscriber::init();
    
    let pool = sqlx::PgPool::connect(&std::env::var("DATABASE_URL").unwrap())
        .await
        .expect("Failed to connect to database");
    
    let state = Arc::new(AppState { pool });
    
    let app = Router::new()
        .route("/api/users", get(list_users).post(create_user))
        .route("/api/users/:id", get(get_user).put(update_user).delete(delete_user))
        .with_state(state)
        .layer(TraceLayer::new_for_http())
        .layer(CorsLayer::permissive());
    
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

State Management

#[derive(Clone)]
struct AppState {
    pool: sqlx::PgPool,
}

// Share state via Arc
type SharedState = Arc<AppState>;

Handlers with Extractors

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

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

#[derive(Serialize, sqlx::FromRow)]
struct User {
    id: uuid::Uuid,
    name: String,
    email: String,
    created_at: chrono::DateTime<chrono::Utc>,
}

async fn list_users(
    State(state): State<SharedState>,
    Query(params): Query<ListQuery>,
) -> Result<Json<Vec<User>>, AppError> {
    let page = params.page.unwrap_or(1) as i64;
    let per_page = params.per_page.unwrap_or(20) as i64;
    let offset = (page - 1) * per_page;
    
    let users = sqlx::query_as::<_, User>(
        "SELECT id, name, email, created_at FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2"
    )
    .bind(per_page)
    .bind(offset)
    .fetch_all(&state.pool)
    .await?;
    
    Ok(Json(users))
}

async fn get_user(
    State(state): State<SharedState>,
    Path(id): Path<uuid::Uuid>,
) -> Result<Json<User>, AppError> {
    let user = sqlx::query_as::<_, User>(
        "SELECT id, name, email, created_at FROM users WHERE id = $1"
    )
    .bind(id)
    .fetch_optional(&state.pool)
    .await?
    .ok_or(AppError::NotFound)?;
    
    Ok(Json(user))
}

Request Body Validation

use axum::extract::rejection::JsonRejection;
use validator::Validate;

#[derive(Deserialize, Validate)]
struct CreateUserRequest {
    #[validate(length(min = 2, max = 100))]
    name: String,
    #[validate(email)]
    email: String,
    #[validate(length(min = 8))]
    password: String,
}

async fn create_user(
    State(state): State<SharedState>,
    Json(payload): Json<CreateUserRequest>,
) -> Result<(StatusCode, Json<User>), AppError> {
    payload.validate().map_err(AppError::Validation)?;
    
    let user = sqlx::query_as::<_, User>(
        "INSERT INTO users (id, name, email) VALUES ($1, $2, $3) RETURNING *"
    )
    .bind(uuid::Uuid::new_v4())
    .bind(&payload.name)
    .bind(&payload.email)
    .fetch_one(&state.pool)
    .await?;
    
    Ok((StatusCode::CREATED, Json(user)))
}

Error Handling

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

#[derive(Debug)]
enum AppError {
    NotFound,
    Database(sqlx::Error),
    Validation(validator::ValidationErrors),
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            AppError::NotFound => (
                StatusCode::NOT_FOUND,
                "Resource not found".to_string(),
            ),
            AppError::Database(e) => (
                StatusCode::INTERNAL_SERVER_ERROR,
                format!("Database error: {e}"),
            ),
            AppError::Validation(e) => (
                StatusCode::UNPROCESSABLE_ENTITY,
                format!("Validation error: {e}"),
            ),
        };
        
        (status, Json(serde_json::json!({ "error": message }))).into_response()
    }
}

impl From<sqlx::Error> for AppError {
    fn from(e: sqlx::Error) -> Self {
        AppError::Database(e)
    }
}

Middleware with Tower

use tower_http::auth::RequireAuthorizationLayer;
use tower_http::trace::TraceLayer;
use axum::middleware::{self, Next};

async fn auth_middleware(
    State(state): State<SharedState>,
    mut req: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    let token = req
        .headers()
        .get("Authorization")
        .and_then(|v| v.to_str().ok())
        .and_then(|v| v.strip_prefix("Bearer "))
        .ok_or(StatusCode::UNAUTHORIZED)?;
    
    let claims = verify_jwt(token, &state.jwt_secret)
        .map_err(|_| StatusCode::UNAUTHORIZED)?;
    
    req.extensions_mut().insert(claims);
    Ok(next.run(req).await)
}

// Apply to specific routes
let protected = Router::new()
    .route("/api/profile", get(get_profile))
    .route_layer(middleware::from_fn_with_state(state.clone(), auth_middleware));

Testing

#[cfg(test)]
mod tests {
    use super::*;
    use axum::body::Body;
    use axum::http::{Request, StatusCode};
    use tower::ServiceExt;

    #[tokio::test]
    async fn test_get_user_not_found() {
        let app = create_test_app().await;
        
        let response = app
            .oneshot(
                Request::builder()
                    .uri("/api/users/00000000-0000-0000-0000-000000000000")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        
        assert_eq!(response.status(), StatusCode::NOT_FOUND);
    }
}

Performance Numbers

Axum regularly benchmarks at 500k+ req/s on a single machine — similar to Go's Gin, and far exceeding Python/Node alternatives for CPU-bound work.