No Axum, use State para injetar dependências da aplicação, FromRef para expor partes de AppState e Arc quando precisar compartilhar a mesma alocação entre handlers. Não coloque automaticamente um Mutex ao redor de todo o estado: configuração imutável, pools de banco e contadores têm necessidades diferentes. Já Extension é apropriado para valores transportados nas extensões de uma requisição, frequentemente inseridos por middleware.
Este tutorial usa Axum 0.8.6 e monta um servidor executável com configuração imutável, um contador compartilhado e rotas separadas. Não há banco nem autenticação. A intenção é entender a composição do estado antes de acrescentar dependências ao seu backend Axum.
State, Extension ou uma variável global?
| Necessidade | Escolha inicial | Cuidado |
|---|---|---|
| Configuração carregada no startup | State | Evite ler arquivo ou ambiente a cada request |
| Pool SQLx ou cliente HTTP reutilizável | Campo de AppState | Muitos handles já são baratos de clonar |
| Dependência menor de um módulo | State<T> com FromRef<AppState> | A extração é síncrona; não faça I/O nela |
| Usuário autenticado por middleware | Extensões da requisição, lidas com Extension | A ordem de execução do middleware importa |
| Um contador independente | Arc<AtomicU64> | Atomicidade não resolve invariantes entre vários campos |
| Coleção mutável compartilhada | Arc<Mutex<T>> ou outra estrutura adequada | Considere duração do lock e concorrência |
Uma variável global pode funcionar em casos específicos, mas dificulta substituir dependências e isolar testes. Com State, a função de montagem declara o que a aplicação usa. Para persistência, veja a API REST com PostgreSQL; aqui manteremos a infraestrutura fora do caminho.
1. Crie o projeto
Com uma toolchain stable atualizada e Cargo instalado:
cargo new axum-estado
cd axum-estado
Substitua Cargo.toml por:
[package]
name = "axum-estado"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = { version = "=0.8.6", features = ["macros"] }
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net"] }
[dev-dependencies]
tower = { version = "=0.5.2", features = ["util"] }
As versões exatas de Axum e Tower delimitam as APIs do exemplo, não indicam as versões mais recentes. Guarde o Cargo.lock para registrar também as versões transitivas. A feature macros de Axum é habilitada explicitamente para disponibilizar o derive de FromRef; ela não faz parte das features padrão.
2. Monte o estado e os routers
Substitua src/main.rs pelo código completo:
use axum::{
extract::{FromRef, State},
routing::get,
Json, Router,
};
use serde::Serialize;
use std::sync::{
atomic::{AtomicU64, Ordering},
Arc,
};
#[derive(Clone)]
struct Configuracao {
nome: Arc<str>,
}
#[derive(Clone, FromRef)]
struct AppState {
config: Configuracao,
visitas: Arc<AtomicU64>,
}
impl AppState {
fn novo(nome: &str) -> Self {
Self {
config: Configuracao {
nome: Arc::from(nome),
},
visitas: Arc::new(AtomicU64::new(0)),
}
}
}
#[derive(Serialize)]
struct Info {
nome: String,
}
#[derive(Serialize)]
struct Visitas {
total: u64,
}
async fn info(State(config): State<Configuracao>) -> Json<Info> {
Json(Info {
nome: config.nome.to_string(),
})
}
async fn registrar_visita(
State(visitas): State<Arc<AtomicU64>>,
) -> Json<Visitas> {
let total = visitas.fetch_add(1, Ordering::Relaxed) + 1;
Json(Visitas { total })
}
fn rotas_info() -> Router<AppState> {
Router::new().route("/info", get(info))
}
fn app(state: AppState) -> Router {
Router::new()
.merge(rotas_info())
.route("/visitas", get(registrar_visita))
.with_state(state)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
axum::serve(listener, app(AppState::novo("API de estudo"))).await?;
Ok(())
}
Execute cargo run. Em outro terminal:
curl http://127.0.0.1:3000/info
curl http://127.0.0.1:3000/visitas
curl http://127.0.0.1:3000/visitas
/info retorna {"nome":"API de estudo"}. Em um processo recém-iniciado, as duas chamadas sequenciais a /visitas retornam {"total":1} e {"total":2}. Reiniciar o processo zera o contador: estado compartilhado em memória não é persistência.
O endpoint GET que incrementa o contador é apenas uma demonstração. Não o trate como uma API de negócio cujo GET deveria ser seguro e livre de efeitos solicitados pelo cliente. Prefira métricas em middleware para contar tráfego, ou um método como POST quando a operação contratada é registrar um evento.
3. Entenda o que FromRef faz
O derive de FromRef gera implementações para obter cada tipo de campo a partir de &AppState. No exemplo, o handler info precisa somente de Configuracao; ele não conhece o contador. O handler de visitas pede apenas Arc<AtomicU64>.
A implementação equivalente para a configuração seria:
impl FromRef<AppState> for Configuracao {
fn from_ref(state: &AppState) -> Self {
state.config.clone()
}
}
Esse trecho é uma alternativa à implementação gerada para esse campo, não um bloco para adicionar junto ao derive existente. Implementar o mesmo trait para o mesmo par de tipos duas vezes gera conflito.
FromRef não é um container de injeção de dependências nem um lookup por nome. A resolução acontece por tipos. Se dois campos têm o mesmo tipo, o derive pode tentar gerar implementações conflitantes. Use newtypes para distinguir, por exemplo, BancoLeitura e BancoEscrita, ou implemente manualmente a seleção desejada. Não abra conexões nem faça consultas dentro de from_ref: prepare recursos no startup.
4. Por que Router ainda precisa de estado?
O parâmetro de Router<S> representa o estado que falta fornecer, não simplesmente um objeto que já está guardado dentro dele. rotas_info() devolve Router<AppState> porque seus handlers dependem de valores extraíveis de AppState.
A função app() une os routers e chama with_state(state). Seu retorno Router, isto é, Router<()>, indica que nenhuma dependência de estado ficou pendente para servir a aplicação.
Essa distinção ajuda a interpretar erros em merge, nest e axum::serve:
- Se os subrouters serão montados juntos, declare um tipo de estado compatível entre eles.
- Forneça o estado na fronteira de composição escolhida, como fazemos em
app(). - Não conclua que todo erro envolvendo
Router<AppState>exige adicionar outroArc; muitas vezes o problema é um estado ainda não fornecido ou uma anotação incompatível.
with_state também permite escolher outro tipo de estado pendente em composições mais avançadas. O retorno explícito de app() mantém este exemplo no caso simples e evita ambiguidades.
5. Clone, Arc e locks não significam a mesma coisa
O derive de Clone em AppState clona seus campos. Aqui, clonar Arc mantém a mesma alocação: todas as cópias do estado enxergam o mesmo contador. Isso não faz uma cópia independente de cada visita.
Arc<T> permite propriedade compartilhada, mas não oferece mutação arbitrária de T. A mutação do exemplo é possível porque AtomicU64 disponibiliza operações atômicas. Ordering::Relaxed é suficiente para esse contador independente: não usamos seu valor para publicar ou sincronizar outros dados. Não copie essa escolha para protocolos de concorrência com invariantes adicionais. Veja atomics em Rust.
Para dados mais complexos, escolha a sincronização pelo acesso real:
std::sync::Mutexpode servir para uma seção crítica curta, sem.await, com baixa contenção. Seu lock bloqueia a thread enquanto espera.tokio::sync::Mutexpermite aguardar o lock de modo assíncrono. É útil quando realmente precisa manter exclusividade durante uma operação assíncrona, mas não elimina gargalos.- Um pool como
sqlx::PgPooljá gerencia concorrência e compartilhamento. Envolver o pool inteiro em um mutex normalmente só serializa acesso desnecessariamente.
Evite segurar um guard de std::sync::Mutex através de .await. Além do risco de bloquear o executor, isso pode tornar a future incompatível com os requisitos do handler. Copie ou transforme o necessário em um escopo curto, solte o lock e só depois aguarde I/O. Consulte o guia de Mutex e o diagnóstico future não é Send.
6. Teste compartilhamento e isolamento
Acrescente ao final de src/main.rs:
#[cfg(test)]
mod tests {
use super::*;
use axum::{
body::{to_bytes, Body},
http::{Request, StatusCode},
};
use tower::ServiceExt;
async fn visitar(router: Router) -> String {
let request = Request::builder()
.uri("/visitas")
.body(Body::empty())
.unwrap();
let response = router.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let bytes = to_bytes(response.into_body(), 1024).await.unwrap();
String::from_utf8(bytes.to_vec()).unwrap()
}
#[tokio::test]
async fn clones_compartilham_contador() {
let router = app(AppState::novo("teste"));
assert_eq!(visitar(router.clone()).await, r#"{"total":1}"#);
assert_eq!(visitar(router).await, r#"{"total":2}"#);
}
#[tokio::test]
async fn estados_novos_sao_independentes() {
let primeiro = app(AppState::novo("primeiro"));
let segundo = app(AppState::novo("segundo"));
assert_eq!(visitar(primeiro).await, r#"{"total":1}"#);
assert_eq!(visitar(segundo).await, r#"{"total":1}"#);
}
}
Rode cargo test: os dois testes devem passar sem abrir uma porta. O primeiro comprova que clones do router compartilham o recurso. O segundo comprova que construir um novo AppState produz outra fixture. Clonar estado não isola testes; recriar ou substituir suas dependências pode isolar.
Comparamos aqui um JSON minúsculo de um único campo como texto. Para respostas maiores, prefira comparar valores desserializados, como no tutorial de testes Axum sem servidor HTTP.
Perguntas frequentes
State substitui Extension em todos os casos?
Não. State expressa dependências da aplicação no tipo do router. Extension lê um valor das extensões da requisição e é útil quando middleware insere informações daquele request. Usar Extension<T> obrigatório sem instalar o valor provoca uma rejection em runtime; com State, a compatibilidade do estado é verificada na composição dos tipos.
Preciso de Arc?
Não necessariamente. Neste projeto, AppState já contém campos compartilhados e baratos de clonar. Envolver todo o objeto em Arc é outra opção válida, mas muda o tipo de estado e as implementações de FromRef necessárias. Escolha uma estratégia consistente em vez de empilhar wrappers sem necessidade.
FromRef deixa o handler independente de tudo?
Ele reduz o acoplamento ao estado principal, mas o handler continua dependente do tipo solicitado. Para substituir um cliente real por uma implementação de teste, você ainda precisa projetar essa dependência — por exemplo, com traits ou uma camada de serviço. FromRef apenas faz a extração.
Esse contador funciona entre várias instâncias?
Não. Cada processo tem seu próprio AppState. Para um valor global entre réplicas, escolha armazenamento externo e defina consistência, durabilidade e política de falhas. Para contagem de tráfego, use uma estratégia de métricas Prometheus adequada à agregação de instâncias.
Checklist e referências
Antes de adicionar uma dependência ao estado, confirme:
- Ela é criada no startup, não em cada handler?
- Clonar o handle compartilha o recurso ou copia dados caros?
- O handler precisa do estado inteiro ou de uma parte via
FromRef? - Há lock atravessando
.awaitou um gargalo por contenção? - Os testes criam fixtures independentes quando necessário?
- A aplicação depende de memória local ou exige persistência entre processos?
Referências das APIs usadas:
Como próximo exercício, substitua o contador por um serviço de aplicação e teste duas requisições que usam a mesma dependência. Para organizar os fundamentos antes de ampliar o backend, siga a trilha do curso de Rust.