An experimental framework for more easily building full-stack applications with
axum
.
While building applications with axum
+ HTMX, the same code was being written over and over again.
This framework is an attempt to reduce boilerplate by deriving it from structure definitions. This
is different from something like loco
where you would use a CLI scaffolding tool to generate both
structures and code. With anodyne
you start from data definitions and the code is generated for
you.
Many frameworks take a highly-modular approach which requires writing a lot of glue code, and makes it more difficult to provide tight integration between concerns.
As an example: there are several excellent validation libraries for rust/axum, but this still requires you to manually run validation in every single handler.
For a simple backend API this is fine, but for building full-stack applications it becomes tedious and error-prone.
Implementation is changing rapidly as details are worked out, but here's what's implemented so far:
As an example:
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/my-form", get(show_form).post(handle_form))
// layer in anodyne middlewares (these will eventually be merged into a single service)
// TODO: update this once middleware has been finalized
;
let listener = tokio::net::TcpListener::bind("127.0.0.1:4000").await.unwrap();
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
.await
.unwrap();
}
#[derive(Deserialize, Serialize, Form, Default, Debug)]
pub struct MyForm {
// HTML input name will be `exact_length`, but label will be as specified in attribute
#[form(
label="Enter eight characters"
// Validation will ensure length of value is exactly 8 characters
len=8,
// Validation will ensure input matches this regex
regex="^[A-Za-z0-9]+$",
// Validation will show this error if regex doesn't match
regex_description="must be only alphanumeric characters"
)]
exact_length: String
#[form(
// You can also use range specifiers for length validation
len=8..=32
)]
password: String,
#[form(
// You can reference a field in this struct which this field must match
matches=password
// The label for this field will be "Confirm Password" (automatically converted from
// snake-case)
)]
confirm_password: String,
}
async fn show_form() -> AnodyneResult<MyForm> {
Ok(
AnodyneResponse::from_data(
// Here we specify that the form is initialized as empty, but you could provide some
// pre-filled values if you wanted by passing a filled-in struct.
MyForm::default()
)
.as_post() // Method for this form will be POST
.with_route("/my-form") // Where data will be posted
)
}
// This function could return AnodyneResult, or you can freely mix/match axum responses
async fn handle_form(FormData(form): FormData<MyForm>) -> impl IntoResponse {
// You can access data in `form` and it's already valid by this point. If the user caused
// a validation error they've already been redirected to the form to correct those errors.
}
Accept
header sent in a request.