Pitch: Recriei o Express em Rust (rpress)
Fala, pessoal
Gostaria de compartilhar com vocês um projeto que venho desenvolvendo: o rpress. Como o nome sugere, a ideia foi criar um framework HTTP em Rust fortemente inspirado na simplicidade e na experiência de desenvolvimento do Express.js, mas aproveitando todo o poder, segurança e performance que o ecossistema Rust oferece.
Por que recriar o Express em Rust?
O ecossistema Rust já possui frameworks incríveis como Axum e Actix-web. No entanto, minha motivação com o rpress foi criar algo que fosse:
- Familiar: Para quem vem do Node.js, a transição deve ser o mais suave possível.
- Bateria Inclusa (mas leve): Funcionalidades como CORS, Rate Limiting, Compressão e TLS já vêm integradas de forma nativa, sem precisar de dezenas de crates externos para o básico.
- Organização e Reusabilidade com Structs: Diferente de outros frameworks como o Axum, o rpress permite associar rotas diretamente a implementações de structs, o que facilita a organização do código e a reusabilidade de lógica de negócio.
- Focado em Observabilidade: O rpress já nasce com suporte a tracing estruturado, facilitando a integração com ferramentas como Jaeger e Datadog.
Principais Funcionalidades
O rpress foi construído sobre o Tokio e já conta com:
Roteamento baseado em Trie: Suporte a rotas estáticas, dinâmicas e múltiplos métodos.
- Middleware: Global e por grupo de rotas (exatamente como no Express).
- HTTP/2 & TLS: Suporte nativo via h2 e rustls.
- Streaming de Body: Para lidar com grandes volumes de dados sem estourar a memória.
- Segurança Nativa: Headers de segurança automáticos, limites de tamanho de body e Rate Limiting plugável.
- CORS: Implementação robusta seguindo a RFC.
- Organização com Structs: A capacidade de associar rotas diretamente a métodos de structs permite uma arquitetura mais modular e organizada, facilitando a manutenção e o crescimento de aplicações complexas.
Como é o código?
Abaixo um exemplo simples de como subir um servidor:
use rpress::{Rpress, RpressCors, RpressRoutes, RequestPayload, ResponsePayload};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
let cors = RpressCors::new()
.set_origins(vec!["*"])
.set_methods(vec!["GET", "POST", "PUT", "DELETE"])
.set_headers(vec!["Content-Type", "Authorization"]);
let mut app = Rpress::new(Some(cors));
let mut routes = RpressRoutes::new();
routes.add(":get/hello", |_req: RequestPayload| async move {
ResponsePayload::text("Hello, Rpress!")
});
app.add_route_group(routes);
app.listen("0.0.0.0:3000").await?;
Ok(())
}
Exemplos de middleware
Middleware globais
Aplicado a todas as rotas
app.use_middleware(|req, next| async move {
let uri = req.uri().to_string();
let method = req.method().to_string();
tracing::info!("--> {} {}", method, uri);
let start = std::time::Instant::now();
let result = next(req).await;
tracing::info!("<-- {} {} ({:?})", method, uri, start.elapsed());
result
});
Middlware por grupo de rotas
let mut routes = RpressRoutes::new();
routes.use_middleware(|req, next| async move {
if req.header("authorization").is_none() {
return Err(RpressError {
status: StatusCode::Unauthorized,
message: "Token required".to_string(),
});
}
next(req).await
});
routes.add(":get/admin/dashboard", |_req: RequestPayload| async move {
ResponsePayload::text("Admin area")
});
A cereja do bolo
Essa funcionalidade foi, para mim, uma das principais motivações para criar o meu próprio framework. Já usei Actix e Axum por muito tempo em diversos projetos, e a coisa que mais me incomoda neles é o fato de não conseguir organizar minhas rotas junto com as implementações das minhas structs.
Segue abaixo um exemplo de como resolvi esse problema.
use rpress::handler;
pub struct UserController;
impl UserController {
pub fn new() -> Arc<Self> {
Arc::new(Self)
}
async fn get_user(&self, req: RequestPayload) -> Result<ResponsePayload, RpressError> {
let id = req.get_param("id").ok_or_else(|| RpressError {
status: StatusCode::BadRequest,
message: "Missing id".to_string(),
})?;
Ok(ResponsePayload::json(&serde_json::json!({
"id": id,
"name": "Guilherme"
}))?)
}
async fn create_user(&self, mut req: RequestPayload) -> Result<ResponsePayload, RpressError> {
let body = req.collect_body().await;
let data: serde_json::Value = serde_json::from_slice(&body)?;
Ok(ResponsePayload::json(&serde_json::json!({
"created": true,
"name": data["name"]
}))?.with_status(StatusCode::Created))
}
}
pub fn get_user_routes() -> RpressRoutes {
let controller = UserController::new();
let mut routes = RpressRoutes::new();
routes.add(":get/users/:id", handler!(controller, get_user));
routes.add(":post/users", handler!(controller, create_user));
routes
}
Onde encontrar?
O projeto é open-source e adoraria receber feedbacks, sugestões ou contribuições da comunidade!
- Crates.io: https://crates.io/crates/rpress
- Repositório: https://github.com/Guilherme-j10/rpress
O que acharam da proposta? Rust está cada vez mais maduro para web e projetos como esse ajudam a baixar a barreira de entrada para novos desenvolvedores na linguagem, pelo menos é o que eu acredito.