Rocket Rust: Tutorial de API REST com Web Framework | Rust Brasil

Aprenda Rocket Rust 0.5: crie e teste seu primeiro servidor, use rotas, request guards, JSON, managed state, fairings e templates e avance para produção com exemplos em português.

Rocket é o framework web em Rust com a API mais declarativa do ecossistema: rotas definidas por macros de atributo, dados de requisição validados por tipos, estado compartilhado gerenciado pelo próprio framework e middleware (fairings) anexado explicitamente. Desde a série 0.5, o Rocket é assíncrono por completo, roda sobre o Tokio e trata a maioria das decisões de uma API web — validação de formulários, cookies, sessões, templates e pools de banco — com módulos oficiais em vez de escolhas manuais de crates.

Se você pesquisou rocket rust, rocket framework ou rust web framework, esta página explica o que o Rocket é, como instalar e como escrever sua primeira API — com exemplos em português e o mesmo caminho progressivo usado nos guias de Axum e Actix Web.

# Começo mínimo
[dependencies]
rocket = "0.5"
serde = { version = "1", features = ["derive"] }
#[macro_use] extern crate rocket;

#[get("/")]
fn raiz() -> &'static str {
    "Olá, mundo!"
}

#[rocket::main]
async fn main() {
    let _ = rocket::build()
        .mount("/", routes![raiz])
        .launch()
        .await
        .expect("Falha ao iniciar o Rocket");
}

Comece aqui: do projeto vazio ao primeiro servidor Rocket

Com Rust e o Cargo instalados, crie um projeto de estudo e adicione as dependências:

cargo new minha-api-rocket
cd minha-api-rocket
cargo add rocket serde --features serde/derive

Substitua o src/main.rs pelo exemplo Hello World acima e rode:

cargo run

O servidor sobe em http://localhost:8000 (o endereço padrão do Rocket em ambiente debug) e o log do terminal mostra a tabela de rotas montadas — o framework imprime cada rota com o método, o caminho e o nome do handler. Não é necessário instalar banco de dados nem configurar templates para testar a primeira rota.

Antes de avançar, três diferenças de mentalidade em relação a Axum e Actix Web:

  1. Rotas são atributos dos handlers. O #[get("/caminho")] declara rota e handler juntos; não existe uma tabela central de rotas.
  2. Parâmetros são tipados na assinatura. O Rocket infere o que cada argumento significa pelo tipo: &str em path, guard de query, State<T> etc.
  3. Middleware é attach, não wrap. Comportamentos globais entram com .attach(fairing) em vez de envolver cada rota.

Instalação

O Rocket 0.5 exige uma versão de Rust razoavelmente recente — recomenda-se manter a toolchain atualizada com rustup:

rustup update stable
cargo add rocket

Dependências comuns conforme o guia avança:

[dependencies]
rocket = { version = "0.5", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

A feature json habilita o suporte nativo a corpos JSON via Serde: o extractor Json<T> para entrada e o responder para saída. Para templates dinâmicos, banco de dados, cookies privados e arquivos estáticos existem crates oficiais (rocket_dyn_templates, rocket_db_pools), apresentadas nas seções seguintes.

Verifique a instalação com o build de produção, que também mostra avisos de código morto:

cargo build --release

Uso Básico

Hello World

O menor servidor Rocket útil:

#[macro_use] extern crate rocket;

#[get("/")]
fn raiz() -> &'static str {
    "Olá, mundo!"
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![raiz])
}

A macro #[launch] é um atalho: ela gera o main assíncrono, sobe o servidor e gerencia o shutdown sozinha. Quando você precisa de controle fino (configurar endereço, aguardar shutdown graceful), troque para #[rocket::main] e chame .launch().await manualmente.

Rotas, paths dinâmicos e query strings

Rotas declaram parâmetros dinâmicos direto no atributo. O tipo do argumento define como o valor é lido e validado:

use rocket::response::Debug;

#[get("/ola/<nome>")]
fn saudacao(nome: &str) -> String {
    format!("Olá, {}!", nome)
}

// Parâmetro tipado: um id inválido nem chega ao handler
#[get("/usuarios/<id>")]
fn obter_usuario(id: u32) -> String {
    format!("Usuário {}", id)
}

// Parâmetros opcionais em query string: ?pagina=2&limite=10
#[get("/produtos?<pagina>&<limite>")]
fn listar_produtos(pagina: Option<u32>, limite: Option<u32>) -> String {
    let pagina = pagina.unwrap_or(1);
    let limite = limite.unwrap_or(20);
    format!("Página {} com até {} produtos", pagina, limite)
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![saudacao, obter_usuario, listar_produtos])
}

Ponto central do design do Rocket: /usuarios/<id> com id: u32 não aceita /usuarios/abc. A requisição inválida é roteada para o próximo handler compatível (ou vira 404/422), sem uma linha de validação escrita por você. Um id negativo, uma string não numérica ou um overflow retornam erro antes do handler rodar — validação de entrada pela assinatura da função, não por ifs.

Ranking de rotas: quando dois handlers podem atender à mesma requisição, o Rocket usa a ordem declarada em routes![] como prioridade e o ranking automático de especificidade (rotas com menos parâmetros dinâmicos ganham de rotas com mais). Na prática, declare a rota mais específica primeiro e teste conflitos com o framework de testes.

JSON de entrada e saída

Com a feature json, o Rocket integra Serde de ponta a ponta:

use rocket::serde::{Deserialize, Serialize, json::Json};

#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
struct NovoProduto<'r> {
    nome: &'r str,
    preco_em_centavos: u64,
}

#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
struct Produto {
    id: u32,
    nome: String,
    preco_em_centavos: u64,
}

#[post("/produtos", data = "<novo>")]
fn criar_produto(novo: Json<NovoProduto>) -> Json<Produto> {
    let produto = Produto {
        id: 1,
        nome: novo.nome.to_string(),
        preco_em_centavos: novo.preco_em_centavos,
    };
    Json(produto)
}

O argumento data = "<novo>" liga o corpo da requisição ao extractor Json<T>: um corpo malformado ou que não casa com o schema devolve 422 Unprocessable Entity automaticamente. Devolver Json(produto) serializa a resposta com o Content-Type: application/json.

Para respostas com controle de status, use Result com um responder de erro:

use rocket::response::status;
use rocket::http::Status;

#[get("/produtos/<id>")]
fn obter_produto(id: u32) -> Result<Json<Produto>, status::NotFound<String>> {
    if id == 1 {
        Ok(Json(Produto { id, nome: "Café".into(), preco_em_centavos: 1500 }))
    } else {
        Err(status::NotFound(format!("Produto {} não encontrado", id)))
    }
}

Managed state: compartilhando estado entre handlers

Estado global — pools de banco, clientes HTTP, configuração — entra no builder com .manage() e chega nos handlers como &State<T>:

use rocket::State;
use std::sync::atomic::{AtomicUsize, Ordering};

struct Contador {
    visitas: AtomicUsize,
}

#[get("/visitas")]
fn visitas(contador: &State<Contador>) -> String {
    let atual = contador.visitas.fetch_add(1, Ordering::SeqCst) + 1;
    format!("Esta página foi vista {} vezes", atual)
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .manage(Contador { visitas: AtomicUsize::new(0) })
        .mount("/", routes![visitas])
}

Regras práticas do managed state:

  • O Rocket exige que o tipo seja Send + Sync + 'static; pools como o do SQLx já satisfazem isso.
  • Um único valor por tipo: para dois pools do mesmo tipo, encapsule em uma struct.
  • O State é uma referência barata — não clone o conteúdo só para passar adiante.

Formulários, cookies e sessões

O Rocket nasceu com suporte de primeira classe a HTML forms, o que o torna forte em aplicações web tradicionais, não só em APIs:

use rocket::form::Form;

#[derive(FromForm)]
struct Login<'r> {
    usuario: &'r str,
    senha: &'r str,
}

#[post("/login", data = "<form>")]
fn login(form: Form<Login>) -> String {
    format!("Usuário {} autenticado", form.usuario)
}

Cookies vêm tipados com &CookieJar e o Rocket oferece cookies privados (assinados) para sessões leves:

use rocket::http::{Cookie, CookieJar};

#[post("/sessao")]
fn criar_sessao(jar: &CookieJar<'_>) -> &'static str {
    jar.add_private(Cookie::new("usuario", "maria"));
    "Sessão criada"
}

#[get("/sessao")]
fn ler_sessao(jar: &CookieJar<'_>) -> String {
    match jar.get_private("usuario") {
        Some(cookie) => format!("Sessão de {}", cookie.value()),
        None => "Sem sessão".into(),
    }
}

Recursos Avançados

Request guards: autenticação como tipo

O mecanismo mais elegante do Rocket é o guard: um tipo que decide se a requisição chega ao handler. Autenticação vira parte da assinatura:

use rocket::request::{self, Request, FromRequest};
use rocket::outcome::Outcome;

struct UsuarioAutenticado {
    nome: String,
}

#[rocket::async_trait]
impl<'r> FromRequest<'r> for UsuarioAutenticado {
    type Error = ();

    async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
        match req.headers().get_one("Authorization") {
            Some(token) if token == "Bearer token-secreto" => {
                Outcome::Success(UsuarioAutenticado { nome: "maria".into() })
            }
            _ => Outcome::Error((rocket::http::Status::Unauthorized, ())),
        }
    }
}

#[get("/painel")]
fn painel(usuario: UsuarioAutenticado) -> String {
    format!("Bem-vinda ao painel, {}", usuario.nome)
}

Sem token válido, /painel responde 401 Unauthorized sem o handler executar. O mesmo mecanismo serve para rate limits por IP, injeção de correlação de tracing, idioma preferido e qualquer decisão pré-handler. Guards compõem: um handler pode exigir UsuarioAutenticado e Admin como argumentos consecutivos.

Fairings: middleware explícito

Fairings são hooks globais anexados com .attach(). Um fairing de log simples:

use rocket::{Request, Data, fairing::{Fairing, Kind, Info}};
use rocket::futures;

struct LogRequisicoes;

#[rocket::async_trait]
impl Fairing for LogRequisicoes {
    fn info(&self) -> Info {
        Info { name: "Log de requisições", kind: Kind::Request }
    }

    async fn on_request(&self, req: &mut Request<'_>, _data: &mut Data<'_>) {
        println!("{} {}", req.method(), req.uri());
    }
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .attach(LogRequisicoes)
        .mount("/", routes![raiz])
}

Hooks disponíveis: on_request (antes do roteamento), on_response (antes de enviar a resposta — ideal para cabeçalhos de segurança), on_launch, on_shutdown e on_ignite. O Rocket já traz fairings prontos, como o de coleta de métricas. A diferença cultural para o ecossistema Tower: fairings são globais e explícitos; layers Tower envolvem serviços individualmente e são reutilizáveis entre frameworks.

Banco de dados com rocket_db_pools

A crate oficial rocket_db_pools integra pools de conexão ao managed state, com suporte a PostgreSQL via SQLx, SQLite, MySQL e MongoDb:

[dependencies]
rocket = { version = "0.5", features = ["json"] }
rocket_db_pools = { version = "0.2", features = ["sqlx_postgres"] }
sqlx = { version = "0.8", default-features = false, features = ["postgres", "macros", "runtime-tokio"] }
use rocket_db_pools::{Database, Connection};
use rocket::{Build, Rocket};

#[derive(Database)]
#[database("meubanco")]
struct MeuBanco(sqlx::PgPool);

#[get("/contagem")]
async fn contagem(db: &MeuBanco) -> Result<String, String> {
    let row: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM produtos")
        .fetch_one(&**db)
        .await
        .map_err(|e| e.to_string())?;
    Ok(format!("{} produtos", row.0))
}

#[rocket::launch]
fn rocket() -> _ {
    rocket::build()
        .attach(MeuBanco::init())
        .mount("/", rocket::routes![contagem])
}

A string meubanco aponta para a configuração em Rocket.toml:

[default.databases.meubanco]
url = "postgres://app:senha@localhost/app"

O pool é criado, gerenciado e encerrado junto com o ciclo de vida da aplicação. Para queries complexas e migrações, o fluxo do tutorial de SQLx aplica igualmente em serviços Rocket.

Templates dinâmicos com Tera

Para páginas HTML renderizadas no servidor, rocket_dyn_templates traz o motor Tera embutido:

use rocket_dyn_templates::{Template, context};

#[get("/pagina")]
fn pagina() -> Template {
    Template::render("pagina", context! {
        titulo: "Início",
        itens: vec!["Café", "Chimarrão", "Pão de queijo"],
    })
}

Com fairings e templates, o Rocket cobre o ciclo completo de uma aplicação web monolítica em uma única stack — um perfil diferente do backend de API puro.

Testes

O Rocket traz um framework de testes local que dispara requisições reais contra a aplicação em memória:

#[cfg(test)]
mod testes {
    use super::*;
    use rocket::local::asynchronous::Client;
    use rocket::uri;

    #[rocket::async_test]
    async fn raiz_retorna_ola() {
        let client = Client::tracked(rocket()).await.expect("cliente válido");
        let resposta = client.get(uri!(raiz)).dispatch().await;
        assert_eq!(resposta.into_string().await, Some("Olá, mundo!".into()));
    }
}

Client::tracked mantém cookies entre requisições — perfeito para testar sessões e autenticação. A macro uri! gera a URI a partir da rota declarada, então renomear caminhos quebra o teste em tempo de compilação, não em produção.

Configuração por ambiente

Todo o comportamento de runtime vem do Rocket.toml (perfis default, debug, release) ou de variáveis de ambiente ROCKET_:

[default]
address = "0.0.0.0"
port = 8000
workers = 8

[release]
log_level = "warn"
ident = "minha-api"

Segredos não vão para o arquivo versionado: use ROCKET_... no ambiente do deploy ou leia via crate de configuração como a Config, integrando com o managed state.

Deploy em produção

O caminho de produção para um binário Rocket segue o padrão do ecossistema:

FROM rust:1 AS builder
WORKDIR /app
COPY . .
RUN cargo build --release

FROM debian:bookworm-slim
COPY --from=builder /app/target/release/minha-api-rocket /usr/local/bin/app
USER 1000
CMD ["./app"]

No VPS, systemd ou Docker Compose gerenciam o processo, com ROCKET_ADDRESS=0.0.0.0 e ROCKET_PORT definidos no ambiente. O guia de deploy de Rust em VPS com Docker e systemd cobre o fluxo completo, e o de builds Docker otimizados mostra como encurtar o build multi-stage.

Boas Práticas

  1. Valide pela assinatura. Prefira parâmetros tipados (id: u32, guards de query, FromForm) a validar strings dentro do handler. Se a validação cabe num tipo, o Rocket faz antes do handler rodar.
  2. Um tipo por estado, um estado por aplicação. Estruture todo o estado compartilhado numa única struct de aplicação e registre-a uma vez com .manage().
  3. Autenticação como guard, não como if. Transforme políticas de acesso em tipos que implementam FromRequest e componha guards em vez de repetir checagens.
  4. Erros como tipos. Modele erros com Thiserror e implemente Responder no tipo de erro da aplicação — handlers devolvem Result<Resposta, ErroApp> sem unwrap.
  5. Teste com Client::tracked e uri!. O custo de escrever testes de integração no Rocket é próximo de zero; use-o para travar contratos de rota.
  6. Observe com fairing + Tracing. Um fairing de on_request/on_response abre spans por requisição e integra com o resto da stack de observabilidade.
  7. Não reinvente módulos oficiais. Antes de escolher crates de comunidade para templates, cookies, DB pools e WebSockets, verifique a família rocket_* — a integração com o ciclo de vida da aplicação já vem pronta.

Exemplos Práticos

API REST completa

#[macro_use] extern crate rocket;

use rocket::serde::{Serialize, json::Json};
use rocket::response::status;
use rocket::{State,serde::Deserialize};
use std::sync::Mutex;

#[derive(Serialize, Deserialize, Clone)]
#[serde(crate = "rocket::serde")]
struct Tarefa {
    id: u32,
    titulo: String,
    feita: bool,
}

struct Banco {
    tarefas: Mutex<Vec<Tarefa>>,
    proximo_id: std::sync::atomic::AtomicU32,
}

#[get("/tarefas")]
fn listar(banco: &State<Banco>) -> Json<Vec<Tarefa>> {
    let tarefas = banco.tarefas.lock().unwrap().clone();
    Json(tarefas)
}

#[post("/tarefas", data = "<nova>")]
fn criar(
    nova: Json<std::collections::HashMap<String, String>>,
    banco: &State<Banco>,
) -> status::Created<Json<Tarefa>> {
    use std::sync::atomic::Ordering;
    let id = banco.proximo_id.fetch_add(1, Ordering::SeqCst) + 1;
    let tarefa = Tarefa {
        id,
        titulo: nova.get("titulo").cloned().unwrap_or_default(),
        feita: false,
    };
    banco.tarefas.lock().unwrap().push(tarefa.clone());
    status::Created::new("/tarefas").body(Json(tarefa))
}

#[delete("/tarefas/<id>")]
fn remover(id: u32, banco: &State<Banco>) -> status::NoContent {
    banco.tarefas.lock().unwrap().retain(|t| t.id != id);
    status::NoContent
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .manage(Banco {
            tarefas: Mutex::new(vec![
                Tarefa { id: 1, titulo: "Estudar ownership".into(), feita: false },
            ]),
            proximo_id: std::sync::atomic::AtomicU32::new(1),
        })
        .mount("/", routes![listar, criar, remover])
}

Comparação com Alternativas

CaracterísticaRocketAxumActix Web
PerformanceBoaExcelenteExcelente (top benchmarks)
MaturidadeMadura (desde 2016)Jovem (desde 2021)Muito madura (desde 2017)
API styleMacros de atributo, declarativaFunções + composiçãoMacros + config
MiddlewareFairingsTower (padrão do ecossistema)Próprio (Transform)
EstadoManaged state (&State<T>)State extractorweb::Data (Arc)
Validação de entradaTipos na assinaturaExtractorsExtractors
Formulários e sessõesOficiais, integradosVia cratesVia crates
TemplatesTera integrado (rocket_dyn_templates)Tera, AskamaTera, Askama
TestesFramework integrado com cookiesVia tower::ServiceExtFramework integrado
EcossistemaModeradoGrande e crescendoGrande

O Rocket se destaca por:

  • Ergonomia máxima: menos código por rota do que qualquer concorrente direto
  • Validação por tipos na assinatura: erros de entrada respondidos antes do handler
  • Batteries included: formulários, cookies privados, templates e DB pools oficiais
  • Framework de testes integrado com Client::tracked para sessões
  • Ótimo para aplicações full-stack monolíticas renderizando HTML no servidor

Quando preferir as alternativas: Axum quando o time já vive no mundo Tower/Tokio ou quer a maior comunidade; Actix Web quando o teto de throughput importa. Os detalhes estão no comparativo Axum vs Actix e na visão geral de Rust para web.

Conclusão

Rocket é o framework web em Rust que mais reduz o código entre a ideia e a primeira API funcionando: rotas declarativas, entrada validada por tipos, estado gerenciado, fairings explícitos e módulos oficiais para o resto. Para aplicações full-stack com HTML, formulários e sessões, é a stack mais completa em uma única família de crates.

Pontos-chave para lembrar:

  • #[get], #[post] e companhia declaram rota e handler juntos
  • Parâmetros tipados na assinatura validam path e query antes do handler
  • .manage() + &State<T> para pools, clientes e configuração
  • Request guards transformam autenticação em tipo, com 401 automático
  • Fairings com .attach() para middleware global de log, headers e métricas
  • rocket_dyn_templates e rocket_db_pools para HTML e banco sem caça a crates
  • Client::tracked + uri! para testes de integração baratos

Para aprofundar, consulte a documentação oficial do Rocket e os exemplos no GitHub.

Para comparar antes de decidir, veja os guias de Axum, Actix Web e o comparativo entre os dois, ou siga o tutorial de API REST com Axum quando quiser a experiência no estilo Tower.

Perguntas frequentes sobre Rocket

O que é o Rocket?

É um framework web em Rust focado em ergonomia, com rotas via macros de atributo, validação de entrada por tipos, managed state, fairings, templates Tera e framework de testes integrado — assíncrono sobre o Tokio desde a versão 0.5.

Como instalar o Rocket?

cargo add rocket (com a feature json para APIs) via Cargo, mais #[macro_use] extern crate rocket; no main.rs para habilitar as macros de rota.

Rocket ou Axum: qual escolher em 2026?

Rocket quando você quer convenções prontas e uma stack completa para aplicações web tradicionais; Axum quando o valor está na integração com Tower e na maior comunidade do ecossistema. O comparativo entre Axum e Actix Web está em Axum vs Actix.

Como compartilhar estado entre handlers no Rocket?

Com managed state: .manage(valor) no builder e &State<T> nos handlers. O tipo precisa ser Send + Sync, o que pools do SQLx e Arc<Mutex<T>> já satisfazem.

Como autenticar rotas no Rocket?

Implemente FromRequest para um tipo de usuário autenticado (lendo header, cookie privado ou token JWT) e receba-o como argumento nos handlers protegidos — o guard retorna 401 antes de o handler rodar. Para o padrão com Axum, veja autenticação JWT com Axum.

Rocket aparece em vagas no Brasil?

Menos que Axum e Actix Web, mas aparece em serviços internos e startups. O grosso das vagas de Rust pede Axum, Actix Web, Tokio e SQLx; conhecer Rocket acelera protótipos e soma como diferencial. Veja a trilha de carreira backend e os salários de Rust no Brasil.