Rust for Backend Developers: A Practical Getting Started Guide



Rust has gone from “interesting experiment” to “production-ready choice” for backend systems. Discord rewrote their message storage in Rust. Cloudflare runs Rust at the edge. AWS built Firecracker in Rust.

If you’re a backend developer curious about Rust, this guide will get you productive fast.

Code Editor Photo by Ilya Pavlov on Unsplash

Why Rust for Backend?

BenefitWhat It Means
Memory safetyNo null pointers, no data races—at compile time
PerformanceC/C++ speed without the footguns
ConcurrencyFearless concurrency with ownership model
ToolingCargo is arguably the best package manager

Setting Up

# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Verify
rustc --version
cargo --version

Your First HTTP Server with Axum

Axum is the modern choice for Rust web services—built on Tokio, ergonomic, and fast:

cargo new my-api
cd my-api

Add dependencies to Cargo.toml:

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
// src/main.rs
use axum::{routing::get, routing::post, Json, Router};
use serde::{Deserialize, Serialize};

#[derive(Serialize)]
struct Health {
    status: String,
}

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

#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
    email: String,
}

async fn health() -> Json<Health> {
    Json(Health {
        status: "ok".to_string(),
    })
}

async fn create_user(Json(payload): Json<CreateUser>) -> Json<User> {
    Json(User {
        id: 1,
        name: payload.name,
        email: payload.email,
    })
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/health", get(health))
        .route("/users", post(create_user));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    println!("Server running on http://localhost:3000");
    axum::serve(listener, app).await.unwrap();
}
cargo run
# Test it
curl http://localhost:3000/health
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Alex","email":"alex@example.com"}'

Understanding Ownership (The Key Concept)

Rust’s ownership system is what makes it special. Here’s the gist:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // s1 is MOVED to s2
    
    // println!("{}", s1);  // ❌ Error: s1 is no longer valid
    println!("{}", s2);     // ✅ Works
}

For backend work, you’ll mostly use:

// Clone when you need a copy
let s2 = s1.clone();

// References when you just need to read
fn print_len(s: &String) {
    println!("{}", s.len());
}

// Mutable references when you need to modify
fn append_world(s: &mut String) {
    s.push_str(" world");
}

Programming Concept Photo by Kevin Ku on Unsplash

Error Handling Done Right

Rust uses Result<T, E> instead of exceptions:

use std::fs::File;
use std::io::Read;

fn read_config() -> Result<String, std::io::Error> {
    let mut file = File::open("config.toml")?;  // ? propagates errors
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

fn main() {
    match read_config() {
        Ok(config) => println!("Config: {}", config),
        Err(e) => eprintln!("Failed to read config: {}", e),
    }
}

For web services, use anyhow for application errors and thiserror for library errors:

use anyhow::{Context, Result};

async fn fetch_user(id: u64) -> Result<User> {
    let user = db.get_user(id)
        .await
        .context("Failed to fetch user from database")?;
    Ok(user)
}

Database Access with SQLx

SQLx provides compile-time checked SQL:

[dependencies]
sqlx = { version = "0.7", features = ["runtime-tokio", "postgres"] }
use sqlx::postgres::PgPoolOptions;

#[derive(sqlx::FromRow)]
struct User {
    id: i64,
    name: String,
    email: String,
}

async fn get_user(pool: &sqlx::PgPool, id: i64) -> Result<User, sqlx::Error> {
    sqlx::query_as!(
        User,
        "SELECT id, name, email FROM users WHERE id = $1",
        id
    )
    .fetch_one(pool)
    .await
}

The magic: if your SQL is wrong or types don’t match, it won’t compile.

Async/Await in Rust

Rust’s async is zero-cost—no garbage collector, no runtime overhead:

async fn fetch_all() -> Vec<Data> {
    let (users, orders, products) = tokio::join!(
        fetch_users(),
        fetch_orders(),
        fetch_products(),
    );
    // All three run concurrently
    combine(users, orders, products)
}

Performance Comparison

Real benchmarks from the TechEmpower Framework Benchmarks:

FrameworkRequests/sec (JSON)
Axum (Rust)~700,000
Actix (Rust)~650,000
Go (stdlib)~450,000
Node.js (fastify)~200,000

Learning Path

  1. Week 1-2: Ownership, borrowing, lifetimes
  2. Week 3-4: Structs, enums, pattern matching
  3. Week 5-6: Error handling, traits
  4. Week 7-8: Async/await, web frameworks

Resources

Should You Use Rust?

Yes if:

  • Performance is critical
  • You need memory safety guarantees
  • You’re building infrastructure/systems
  • You want to prevent entire classes of bugs

Maybe not if:

  • Rapid prototyping is priority
  • Team has no systems programming experience
  • Simple CRUD with no performance requirements

Conclusion

Rust has a steep learning curve, but the payoff is real. Once you internalize ownership, you’ll write faster, safer code than in any garbage-collected language.

The compiler is strict because it cares about you. Trust it.


Start your Rust journey: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

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