[ANN] I built a Django/DRF-inspired Rust framework and want feedback on the API design before 0.1

Hi all,

I’ve been working on Reinhardt (reinhardt-web on crates.io, imported as reinhardt), a Rust web framework inspired by Django / DRF.

The motivation was simple: whenever I started a new Rust web project, I kept rebuilding the same stack from separate crates: HTTP/routing, ORM, migrations, dependency injection, auth, background jobs, and API scaffolding. Reinhardt is my attempt to provide those pieces as one integrated system while still keeping them composable.

Current state:

  • release candidate: v0.1.0-rc.14
  • open source: BSD-3-Clause
  • not production-ready yet

What currently works best is the core path:

  • HTTP/routing
  • DI
  • ORM/query building
  • auto-migrations from #[model(...)]
  • REST-style APIs
  • project bootstrapping via CLI

A few parts that may be interesting from an API-design perspective:

  • Composable feature flags: minimal for HTTP/routing/DI, standard for the main web stack, or individual features if you want to assemble a narrower setup.
  • Model-driven migrations: models are declared in Rust and migrations are generated from that.
  • Integrated Pages: there is a WASM+SSR “Pages” system for keeping frontend and backend in one Rust project, but this area is still early and I would not compare it to mature standalone frontend frameworks yet.

To be transparent: I used AI assistance heavily for some boilerplate-heavy implementation work, but the API/design decisions, review, crate structure, and release decisions were mine.

Areas that are still underdeveloped:

  • admin UI
  • GraphQL
  • gRPC
  • Pages / broader WASM-side ergonomics
  • deployment/scaffolding niceties

So I’m not posting this as “ready for production”, but as “I think the shape is finally visible, and I’d like feedback before stabilizing it”.

A few questions I’d especially appreciate feedback on:

  1. Does the integrated / batteries-included approach seem useful in Rust, or do you strongly prefer assembling your own stack?
  2. For model-driven migrations, what level of explicit control would you want around destructive changes such as column drops or renames?
  3. If you skim the API, what feels like the biggest design smell or missing piece?
  4. If you were to try this on a side project, what would you need to see first?

Quick start:

reinhardt = { version = "0.1.0-rc.14", package = "reinhardt-web" }
cargo install --locked --version 0.1.0-rc.14 reinhardt-admin-cli
reinhardt-admin startproject my-api
cd my-api
cargo run --bin manage runserver

Links:

If there’s interest, I can follow up with a separate reply containing a few focused code examples for Pages, model definitions, DI, and API View.

1 Like

Here are a few focused code examples, since the API shape is probably easier to judge from code than from a high-level description.

A caveat up front: the core path (routing/DI/ORM/API) is in much better shape than admin / GraphQL / gRPC / broader WASM ergonomics right now.

Pages

Pages is my attempt at a built-in WASM+SSR path for cases where I want frontend and backend in one Rust project.

use reinhardt::pages::server_fn::{server_fn, ServerFnError};
use reinhardt::pages::reactive::hooks::{Action, use_action};
use reinhardt::pages::component::Page;
use reinhardt::pages::page;

#[server_fn]
async fn get_user(#[inject] db: DatabaseConnection, id: i64) -> Result<User, ServerFnError> {
    User::objects().get(id).get_with_db(&db).await.map_err(ServerFnError::from)
}

fn user_profile(id: i64) -> Page {
    let load_user = use_action(move |_: ()| async move {
        get_user(id).await.map_err(|e| e.to_string())
    });
    load_user.dispatch(());

    let load_user_signal = load_user.clone();

    page!(|load_user_signal: Action<User, String>| {
        div {
            watch {
                if let Some(user) = load_user_signal.result() {
                    div {
                        p { { format!("Name: {}", user.name) } }
                        p { { format!("Email: {}", user.email) } }
                    }
                } else if load_user_signal.is_pending() {
                    p { "Loading..." }
                }
            }
        }
    })(load_user_signal)
}

I would not present Pages as a replacement for mature standalone frontend frameworks yet. It is mainly for the "single Rust codebase" use case.

Model definitions and model-driven migrations

#[model(app_label = "users", table_name = "users")]
pub struct CustomUser {
    #[field(primary_key = true)]
    pub id: i64,
    #[field(max_length = 255)]
    pub email: String,
    #[field(default = true)]
    pub is_active: bool,
    #[field(auto_now_add = true)]
    pub created_at: DateTime<Utc>,
}

That definition is intended to be the source for both ORM usage and migration generation.

A query example:

let active_users = CustomUser::objects()
    .filter(CustomUser::field_is_active().eq(true))
    .filter(CustomUser::field_created_at().gte(now))
    .all_with_db(&db)
    .await?;

The field_*() accessors are auto-generated by #[model] and return typed FieldRef<M, T> values, so .eq() / .gte() etc. are compile-time checked against the field type.

One of the things I am still thinking about is how much explicit control should be required for destructive migration steps such as column drops or renames.

DI in handlers

#[get("/users", name = "list_users")]
pub async fn list_users(
    #[inject] db: DatabaseConnection,
) -> ViewResult<Response> {
    let users = User::objects().all().all_with_db(&db).await?;
    let data = ApiResponse::success(users.into_iter().map(UserSerializer::from).collect());
    Response::ok().with_json(&data)
}

This is one of the areas where I wanted a more integrated feel instead of repeatedly wiring the same pieces together for each project.

HTTP method macros

A single-resource endpoint looks like this:

#[get("/users/me", name = "current_user")]
pub async fn current_user(
    #[inject] db: DatabaseConnection,
    request: Request,
) -> ViewResult<Response> {
    let auth = request.extensions().get::<AuthState>()
        .ok_or_else(|| reinhardt::error("Not authenticated"))?;
    let user = User::objects().get(auth.user_id()).get_with_db(&db).await?;
    Response::ok().with_json(&ApiResponse::success(UserSerializer::from(user)))
}

The same pattern works with #[post], #[put], #[patch], #[delete]. There is also a more declarative ViewSet layer for CRUD-style resources, but I thought individual endpoint examples were a better fit for an initial forum follow-up.

If any of these examples look wrong or awkward, that kind of feedback would be especially useful before I try to stabilize the API further.