Why I'm Building Rapina: A Web Framework for APIs You Can Actually Trust

·5 min read

I've been writing APIs for nearly two decades. I've seen them grow, break, and become unmaintainable nightmares. Recently, while working with Loco (a Rails-like framework for Rust), I hit a wall. The codebase was becoming a mess. Conventions were inconsistent. Every endpoint did things slightly differently. And with AI assistants now writing code alongside us, the chaos was multiplying.

That's when I decided to build something different.

The Problem Nobody Names

Modern APIs are easy to write and hard to trust.

They grow fast, break silently, accumulate inconsistencies, and depend on tribal knowledge. Over time, they become hostile territory for maintenance—whether by humans or AI.

I've seen this pattern repeat across companies, teams, and tech stacks. And I realized the root cause isn't the language or the framework. It's the lack of predictability, auditability, and guardrails by default.

The Pain Points

"I don't know if this API is correct"

Handlers accept vague inputs. Responses change without warning. Errors aren't standardized. OpenAPI doesn't reflect reality.

"Every endpoint does things differently"

One endpoint returns { data }, another { user }. Errors vary by module. Auth is applied inconsistently. Observability depends on human discipline.

"Refactoring is terrifying"

A small change breaks the frontend. You can't tell if it's a breaking change. Nobody trusts big refactors. OpenAPI doesn't help.

"AI helps write code, but makes the mess worse"

Generated code has no pattern. Errors are inconsistent. Naming is chaotic. Logic gets duplicated.

"Onboarding is slow and people-dependent"

New devs ask everything. Docs are outdated. Knowledge isn't in the code.

"Production is fragile by default"

Logs are inconsistent. No trace IDs. Timeouts forgotten. Security is optional.

Enter Rapina

Rapina is a web framework for Rust, inspired by FastAPI's developer experience, but built with a different philosophy:

Predictable, auditable, and secure APIs—written by humans, accelerated by AI.

It's not about being "another web framework" or "FastAPI for Rust." It's about solving the trust problem in modern APIs.

What It Looks Like

use rapina::prelude::*;
 
#[derive(Deserialize)]
struct CreateUser {
    name: String,
    email: String,
}
 
#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
    email: String,
}
 
#[get("/users/:id")]
async fn get_user(id: Path<u64>) -> Result<Json<User>> {
    let id = id.into_inner();
 
    if id == 0 {
        return Err(Error::not_found("user not found"));
    }
 
    Ok(Json(User {
        id,
        name: "Antonio".to_string(),
        email: "antonio@example.com".to_string(),
    }))
}
 
#[post("/users")]
async fn create_user(body: Json<CreateUser>) -> Json<User> {
    let input = body.into_inner();
    Json(User {
        id: 1,
        name: input.name,
        email: input.email,
    })
}
 
#[tokio::main]
async fn main() -> std::io::Result<()> {
    let router = Router::new()
        .get("/users/:id", get_user)
        .post("/users", create_user);
 
    Rapina::new()
        .router(router)
        .listen("127.0.0.1:3000")
        .await
}

Clean. Typed. Predictable.

Standardized Errors with Trace IDs

Every error returns a consistent envelope:

{
  "error": {
    "code": "NOT_FOUND",
    "message": "user not found"
  },
  "trace_id": "550e8400-e29b-41d4-a716-446655440000"
}

No more guessing what format an error will be in. No more hunting through logs without a trace ID.

Dependency Injection That Makes Sense

#[derive(Clone)]
struct AppConfig {
    app_name: String,
}
 
#[get("/")]
async fn hello(config: State<AppConfig>) -> String {
    format!("Hello from {}!", config.into_inner().app_name)
}
 
#[tokio::main]
async fn main() -> std::io::Result<()> {
    let config = AppConfig {
        app_name: "My API".to_string(),
    };
 
    Rapina::new()
        .state(config)
        .router(router)
        .listen("127.0.0.1:3000")
        .await
}

Per-Request Dependencies

Need auth? Create a CurrentUser extractor:

struct CurrentUser {
    user_id: u64,
}
 
impl FromRequestParts for CurrentUser {
    async fn from_request_parts(
        parts: &http::request::Parts,
        _params: &PathParams,
        _state: &Arc<AppState>,
    ) -> Result<Self> {
        let user_id = parts
            .headers
            .get("x-user-id")
            .and_then(|v| v.to_str().ok())
            .and_then(|v| v.parse().ok())
            .ok_or_else(|| Error::unauthorized("missing or invalid token"))?;
 
        Ok(CurrentUser { user_id })
    }
}
 
#[get("/me")]
async fn get_me(user: CurrentUser) -> Json<User> {
    Json(User {
        id: user.user_id,
        name: "Current User".to_string(),
        email: "me@example.com".to_string(),
    })
}

No auth header? Automatic 401 with a proper error response. No panic. No surprise.

The Philosophy

Rapina follows three principles:

  • Predictability — Clear conventions, obvious structure. You know what to expect.
  • Auditability — Typed contracts, traceable errors. You can prove it's correct.
  • Security — Guardrails by default. You have to opt-out of safety, not opt-in.

What's Coming

Rapina is still young, but the foundation is solid:

  • [x] Basic router with path parameters
  • [x] Typed extractors (Json, Path, State)
  • [x] Proc macros (#[get], #[post], #[put], #[delete])
  • [x] Standardized error handling with trace_id
  • [x] Dependency injection (State<T>, FromRequestParts)
  • [ ] Query parameters extractor
  • [ ] Validation (Validated<T>)
  • [ ] Auth (Bearer JWT, CurrentUser)
  • [ ] Middleware system
  • [ ] Observability (tracing, structured logs)
  • [ ] Automatic OpenAPI generation
  • [ ] CLI (rapina new, rapina routes, rapina doctor)
  • [ ] Contract-based testing
  • [ ] Breaking change detection

The Ultimate Test

If in 5 years someone can:

  • Understand the API without talking to anyone
  • Refactor without fear
  • Let an AI work on it without creating chaos

Then Rapina will have fulfilled its purpose.

Want to Contribute?

Rapina is open source and I'd love your help. Whether you're coming from Python, Ruby, PHP, or any other ecosystem—if you care about building APIs that are maintainable, predictable, and AI-friendly, come join us.

GitHub: https://github.com/arferreira/rapina

The codebase is clean, the architecture is documented, and there's plenty of low-hanging fruit for first-time contributors:

  • Adding new extractors (Query, Header, Cookie)
  • Improving error messages
  • Writing documentation
  • Adding examples
  • Building the middleware system

Let's build something we can actually trust.

Comments

Participate in the discussion