Executando verificação de segurança...
2

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:

  1. Familiar: Para quem vem do Node.js, a transição deve ser o mais suave possível.
  2. 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.
  3. 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.
  4. 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!

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.

Carregando publicação patrocinada...