Rust and Axum: Secrets to Building High-Speed Web APIs from Real-World Experience

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Why I Went ‘All-In’ on Rust and Axum

After 6 months of putting Rust services into production, I’ve realized one thing: Rust isn’t as ‘hard’ as the rumors say. The biggest challenge is actually getting used to memory management (Ownership). Previously, I often chose Node.js or Go for API development due to coding speed. However, when faced with processing millions of records or requiring latency under 10ms, Rust is a true ‘beast’.

In the Rust ecosystem, Axum has emerged as the most balanced choice. Developed by the team behind Tokio, Axum takes full advantage of Rust’s Type System. This makes your API incredibly safe right from compilation. You’ll rarely encounter runtime errors if your code passes the compiler.

What I love most about Axum is its perfect compatibility with the tower ecosystem. If you’re used to writing middleware in Express or Gin, Axum will feel very familiar. However, it operates at a completely different level of safety and performance.

Project Initialization: Don’t Forget the Standard Boilerplate

To start, ensure you have Rust installed via rustup. We initialize the project with a single command:

cargo new rust-axum-api && cd rust-axum-api

Here is the ‘backbone’ dependency set that I always trust for real-world projects. Update your Cargo.toml file:

[dependencies]
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
sqlx = { version = "0.7", features = ["runtime-tokio-native-tls", "postgres", "macros"] }
dotenvy = "0.15"
tower-http = { version = "0.5", features = ["trace", "cors"] }
tracing = "0.1"
tracing-subscriber = "0.3"

I chose sqlx instead of heavy ORMs. The reason is simple: it allows for SQL syntax error checking during build time. If you misspell a column name, the compiler will catch it immediately, saving hours of debugging in staging environments.

Real-World Configuration: From Database to Routing

1. Database Connection Without Bottlenecks

Instead of using hardcoded connection strings, use a .env file. This makes separating dev and production environments much easier.

DATABASE_URL=postgres://user:password@localhost:5432/my_db

In the main function, we set up the Connection Pool. My experience is to always set max_connections based on server specs. For example, with a 2 vCPU VPS, a value of 5-10 is reasonable to avoid overloading Postgres.

#[tokio::main]
async fn main() {
    dotenvy::dotenv().ok();
    let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL is missing");

    let pool = PgPoolOptions::new()
        .max_connections(10)
        .connect(&db_url)
        .await
        .expect("Failed to connect to database");
}

2. Handling Handlers with Smart Extractors

Axum features a very powerful ‘Extractors’ mechanism. Want to get data from a JSON body or Query string? Just declare the data type in the function parameters. To prepare accurate JSON configurations, I often use JSON Formatter to verify the structure before mapping it to a Rust Struct.

#[derive(Serialize, Deserialize)]
struct User {
    id: i32,
    username: String,
}

async fn get_users(State(pool): State<sqlx::PgPool>) -> Json<Vec<User>> {
    let users = sqlx::query_as!(User, "SELECT id, username FROM users")
        .fetch_all(&pool)
        .await
        .unwrap_or_default();

    Json(users)
}

Production Deployment: The Power of a Single Binary

A common mistake is copying the entire source code to the server and running cargo run. Don’t do that! Rust’s biggest advantage is compiling into a single binary file weighing about 15-20MB. You just need to build the release version:

cargo build --release

Then, copy the file at target/release/rust-axum-api to your Linux server. To keep the service running 24/7, use systemd. Here is a sample config file to help the app restart automatically if it crashes:

[Service]
ExecStart=/var/www/app/rust-axum-api
Restart=always
Environment=DATABASE_URL=postgres://user:password@localhost/db
User=www-data

Impressive Numbers: Performance and RAM

When running in production, you’ll be amazed. A basic Axum API consumes only about 12-15MB of RAM at idle. Meanwhile, equivalent Java or Spring Boot applications usually gobble up at least 200-300MB. Regarding processing speed, I once load-tested with wrk and reached 45,000 requests/second on a low-spec VPS while the CPU remained very stable.

To monitor logs, simply use the command journalctl -u myapp -f. All information from the tracing crate will be clearly displayed, making production debugging no longer a nightmare.

Switching to Rust might take more time initially. However, the peace of mind regarding system stability and drastically optimized server costs are extremely rewarding. Don’t hesitate—try starting with a small microservice today!

Share: