Rust for Backend Development: A Practical Guide for 2026



Rust for Backend Development: A Practical Guide for 2026

Rust has evolved from a systems programming language into a serious contender for backend development. With frameworks like Axum and Actix, building production-ready APIs in Rust is now more accessible than ever.

Programming Code Photo by Ilya Pavlov on Unsplash

Why Rust for Backend?

Performance

Rust consistently outperforms Node.js, Python, and even Go in benchmarks. No garbage collector means predictable latency.

Memory Safety

Rust’s ownership model eliminates entire classes of bugs: null pointer dereferences, buffer overflows, and data races.

Modern Tooling

Cargo, the Rust package manager, provides excellent dependency management, testing, and documentation tools.

Setting Up an Axum Project

cargo new my-api
cd my-api

Add dependencies to Cargo.toml:

[package]
name = "my-api"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.7", features = ["runtime-tokio", "postgres"] }
tower-http = { version = "0.5", features = ["cors", "trace"] }
tracing = "0.1"
tracing-subscriber = "0.3"

Building a REST API

Basic Server Setup

use axum::{
    routing::{get, post},
    Router,
    Json,
};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;

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

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

    let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
    println!("Server running on {}", addr);
    
    axum::serve(
        tokio::net::TcpListener::bind(addr).await.unwrap(),
        app
    ).await.unwrap();
}

async fn root() -> &'static str {
    "Hello, Rust Backend!"
}

Defining Data Models

#[derive(Debug, Serialize, Deserialize)]
struct User {
    id: i64,
    name: String,
    email: String,
    created_at: chrono::DateTime<chrono::Utc>,
}

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

Handler Functions

use axum::{
    extract::{Path, State},
    http::StatusCode,
};

async fn list_users(
    State(pool): State<PgPool>,
) -> Result<Json<Vec<User>>, StatusCode> {
    let users = sqlx::query_as!(User, "SELECT * FROM users")
        .fetch_all(&pool)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    
    Ok(Json(users))
}

async fn get_user(
    State(pool): State<PgPool>,
    Path(id): Path<i64>,
) -> Result<Json<User>, StatusCode> {
    let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
        .fetch_optional(&pool)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
        .ok_or(StatusCode::NOT_FOUND)?;
    
    Ok(Json(user))
}

async fn create_user(
    State(pool): State<PgPool>,
    Json(payload): Json<CreateUser>,
) -> Result<(StatusCode, Json<User>), StatusCode> {
    let user = sqlx::query_as!(
        User,
        r#"
        INSERT INTO users (name, email)
        VALUES ($1, $2)
        RETURNING *
        "#,
        payload.name,
        payload.email
    )
    .fetch_one(&pool)
    .await
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    
    Ok((StatusCode::CREATED, Json(user)))
}

Server Infrastructure Photo by Taylor Vick on Unsplash

Error Handling

Create a custom error type for better error handling:

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

#[derive(Debug)]
pub enum AppError {
    NotFound(String),
    BadRequest(String),
    Internal(String),
    Database(sqlx::Error),
}

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

        let body = Json(serde_json::json!({
            "error": message
        }));

        (status, body).into_response()
    }
}

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

Middleware and Layers

Add common middleware using Tower:

use tower_http::{
    cors::{Any, CorsLayer},
    trace::TraceLayer,
};

let cors = CorsLayer::new()
    .allow_origin(Any)
    .allow_methods(Any)
    .allow_headers(Any);

let app = Router::new()
    .route("/users", get(list_users))
    .layer(TraceLayer::new_for_http())
    .layer(cors)
    .with_state(pool);

Custom Middleware

use axum::{
    middleware::{self, Next},
    http::Request,
};

async fn auth_middleware<B>(
    request: Request<B>,
    next: Next<B>,
) -> Result<Response, StatusCode> {
    let auth_header = request
        .headers()
        .get("Authorization")
        .and_then(|h| h.to_str().ok());

    match auth_header {
        Some(token) if token.starts_with("Bearer ") => {
            // Validate token
            Ok(next.run(request).await)
        }
        _ => Err(StatusCode::UNAUTHORIZED),
    }
}

// Apply to routes
let app = Router::new()
    .route("/protected", get(protected_handler))
    .layer(middleware::from_fn(auth_middleware));

Database Connection Pooling

use sqlx::postgres::PgPoolOptions;

async fn setup_database() -> PgPool {
    let database_url = std::env::var("DATABASE_URL")
        .expect("DATABASE_URL must be set");
    
    PgPoolOptions::new()
        .max_connections(5)
        .acquire_timeout(std::time::Duration::from_secs(3))
        .connect(&database_url)
        .await
        .expect("Failed to create pool")
}

Testing

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

    #[tokio::test]
    async fn test_root() {
        let app = Router::new().route("/", get(root));

        let response = app
            .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::OK);
    }

    #[tokio::test]
    async fn test_create_user() {
        // Setup test database
        let pool = setup_test_database().await;
        let app = create_app(pool);

        let response = app
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/users")
                    .header("Content-Type", "application/json")
                    .body(Body::from(r#"{"name":"Test","email":"test@example.com"}"#))
                    .unwrap(),
            )
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::CREATED);
    }
}

Performance Benchmarks

Typical results for a simple JSON API:

FrameworkRequests/secLatency (p99)
Rust/Axum150,0002ms
Go/Gin120,0003ms
Node/Fastify45,0008ms
Python/FastAPI12,00025ms

Deployment

Dockerfile

FROM rust:1.75 as builder
WORKDIR /app
COPY . .
RUN cargo build --release

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y libssl3 ca-certificates
COPY --from=builder /app/target/release/my-api /usr/local/bin/
EXPOSE 3000
CMD ["my-api"]

Conclusion

Rust for backend development offers:

  • Unmatched performance: Memory-safe without garbage collection
  • Reliability: Compiler catches bugs before runtime
  • Modern ecosystem: Axum, SQLx, and Tokio are production-ready

The learning curve is real, but the payoff in performance and reliability is worth it. Start with a small service and grow from there.


Building backends in Rust? Share your experiences and tips in the comments!

이 글이 도움이 되셨다면 공감 및 광고 클릭을 부탁드립니다 :)