Rust for Backend Development: A Practical Guide for 2026
on Rust, Backend, Api, Performance, Axum
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.
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)))
}
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:
| Framework | Requests/sec | Latency (p99) |
|---|---|---|
| Rust/Axum | 150,000 | 2ms |
| Go/Gin | 120,000 | 3ms |
| Node/Fastify | 45,000 | 8ms |
| Python/FastAPI | 12,000 | 25ms |
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!
이 글이 도움이 되셨다면 공감 및 광고 클릭을 부탁드립니다 :)
