Criar Servidor HTTP em Rust

Aprenda como criar um servidor HTTP em Rust com axum. Rotas, handlers, JSON responses, middleware e estado compartilhado. Exemplos completos.

Criar Servidor HTTP em Rust

O axum é um framework web moderno e ergonômico, construído sobre o ecossistema tokio e tower. Ele oferece rotas tipadas, extração automática de parâmetros e excelente performance.

Dependências

Cargo.toml:

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tower-http = { version = "0.5", features = ["cors"] }

Servidor básico com rotas

Um servidor mínimo com diferentes rotas:

use axum::{routing::get, Router};

async fn index() -> &'static str {
    "Bem-vindo à API Rust!"
}

async fn saude() -> &'static str {
    "OK"
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(index))
        .route("/saude", get(saude));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();

    println!("Servidor rodando em http://localhost:3000");
    axum::serve(listener, app).await.unwrap();
}

Para testar (em outro terminal):

$ curl http://localhost:3000
Bem-vindo à API Rust!

$ curl http://localhost:3000/saude
OK

Respostas JSON

Retorne dados estruturados como JSON:

use axum::{routing::get, Json, Router};
use serde::Serialize;

#[derive(Serialize)]
struct Produto {
    id: u32,
    nome: String,
    preco: f64,
    disponivel: bool,
}

#[derive(Serialize)]
struct ListaProdutos {
    total: usize,
    produtos: Vec<Produto>,
}

async fn listar_produtos() -> Json<ListaProdutos> {
    let produtos = vec![
        Produto {
            id: 1,
            nome: "Notebook".to_string(),
            preco: 4599.90,
            disponivel: true,
        },
        Produto {
            id: 2,
            nome: "Mouse".to_string(),
            preco: 89.99,
            disponivel: true,
        },
        Produto {
            id: 3,
            nome: "Monitor Ultrawide".to_string(),
            preco: 3299.00,
            disponivel: false,
        },
    ];

    let total = produtos.len();
    Json(ListaProdutos { total, produtos })
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/api/produtos", get(listar_produtos));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();

    println!("API rodando em http://localhost:3000");
    axum::serve(listener, app).await.unwrap();
}

Saída (curl):

{
  "total": 3,
  "produtos": [
    {"id": 1, "nome": "Notebook", "preco": 4599.90, "disponivel": true},
    {"id": 2, "nome": "Mouse", "preco": 89.99, "disponivel": true},
    {"id": 3, "nome": "Monitor Ultrawide", "preco": 3299.0, "disponivel": false}
  ]
}

Receber JSON no body (POST)

Aceite dados JSON nas requisições:

use axum::{http::StatusCode, routing::{get, post}, Json, Router};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct NovoProduto {
    nome: String,
    preco: f64,
}

#[derive(Serialize)]
struct ProdutoCriado {
    id: u32,
    nome: String,
    preco: f64,
    mensagem: String,
}

async fn criar_produto(
    Json(novo): Json<NovoProduto>,
) -> (StatusCode, Json<ProdutoCriado>) {
    // Em uma aplicação real, salvaria no banco de dados
    let criado = ProdutoCriado {
        id: 42, // ID gerado
        nome: novo.nome,
        preco: novo.preco,
        mensagem: "Produto criado com sucesso!".to_string(),
    };

    (StatusCode::CREATED, Json(criado))
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/api/produtos", post(criar_produto));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();

    println!("API rodando em http://localhost:3000");
    axum::serve(listener, app).await.unwrap();
}

Para testar:

curl -X POST http://localhost:3000/api/produtos \
  -H "Content-Type: application/json" \
  -d '{"nome": "Teclado Mecânico", "preco": 349.90}'

Saída:

{
  "id": 42,
  "nome": "Teclado Mecânico",
  "preco": 349.9,
  "mensagem": "Produto criado com sucesso!"
}

Parâmetros de rota e query

Extraia parâmetros da URL e query string:

use axum::{
    extract::{Path, Query},
    routing::get,
    Json, Router,
};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct FiltroQuery {
    pagina: Option<u32>,
    limite: Option<u32>,
    busca: Option<String>,
}

#[derive(Serialize)]
struct Resposta {
    mensagem: String,
}

// Parâmetro na URL: /usuarios/42
async fn buscar_usuario(Path(id): Path<u32>) -> Json<Resposta> {
    Json(Resposta {
        mensagem: format!("Usuário #{} encontrado", id),
    })
}

// Query string: /buscar?pagina=1&limite=10&busca=rust
async fn buscar(Query(filtro): Query<FiltroQuery>) -> Json<serde_json::Value> {
    let pagina = filtro.pagina.unwrap_or(1);
    let limite = filtro.limite.unwrap_or(10);
    let busca = filtro.busca.unwrap_or_default();

    Json(serde_json::json!({
        "pagina": pagina,
        "limite": limite,
        "busca": busca,
        "mensagem": format!("Buscando '{}', página {}, {} por página", busca, pagina, limite)
    }))
}

// Múltiplos parâmetros: /api/v1/categorias/eletronicos/produtos/42
async fn produto_por_categoria(
    Path((categoria, produto_id)): Path<(String, u32)>,
) -> Json<Resposta> {
    Json(Resposta {
        mensagem: format!("Produto #{} na categoria '{}'", produto_id, categoria),
    })
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/usuarios/{id}", get(buscar_usuario))
        .route("/buscar", get(buscar))
        .route("/categorias/{categoria}/produtos/{id}", get(produto_por_categoria));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();

    println!("API rodando em http://localhost:3000");
    axum::serve(listener, app).await.unwrap();
}

Saída (exemplos):

GET /usuarios/42
{"mensagem": "Usuário #42 encontrado"}

GET /buscar?pagina=2&limite=5&busca=notebook
{"pagina":2,"limite":5,"busca":"notebook","mensagem":"Buscando 'notebook', página 2, 5 por página"}

Estado compartilhado

Compartilhe dados entre handlers usando State:

use axum::{extract::State, routing::get, Json, Router};
use std::sync::{Arc, Mutex};
use serde::Serialize;

#[derive(Serialize, Clone)]
struct Estatisticas {
    requisicoes: u64,
    ultima_visita: String,
}

type AppState = Arc<Mutex<Estatisticas>>;

async fn estatisticas(State(estado): State<AppState>) -> Json<Estatisticas> {
    let mut stats = estado.lock().unwrap();
    stats.requisicoes += 1;
    stats.ultima_visita = chrono::Utc::now().to_string();
    Json(stats.clone())
}

#[tokio::main]
async fn main() {
    let estado = Arc::new(Mutex::new(Estatisticas {
        requisicoes: 0,
        ultima_visita: String::new(),
    }));

    let app = Router::new()
        .route("/stats", get(estatisticas))
        .with_state(estado);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();

    println!("Servidor rodando em http://localhost:3000");
    axum::serve(listener, app).await.unwrap();
}

Veja também