Groom connects user-defined controllers to an axum router and a utoipa OpenAPI spec. Groom is not a framework. You write handler functions and data types in Rust. Groom wires them into axum routes and adds their paths and schemas to a utoipa OpenApiBuilder.
Groom is inspired by poem-openapi.
use groom::router::GroomRouterValid;
use groom_macros::Controller;
use utoipa::OpenApi;
#[Controller()]
mod api {
use axum::{extract::Query, response::IntoResponse};
use groom::{extract::GroomExtractor, response::Response};
use groom_macros::{DTO, Response};
#[Route(method = "get", path = "/hello")]
pub async fn greet(Query(p): Query<GreetParams>) -> HelloResponse {
let name = p.name.unwrap_or_else(|| "world".into());
if name.is_empty() {
HelloResponse::BadRequest(ErrorMessage {
error: "`name` must be omitted or non-empty",
})
} else {
HelloResponse::Hello(GreetMessage {
message: format!("Hello, {name}!"),
})
}
}
#[DTO(parameters)]
pub struct GreetParams {
name: Option<String>,
}
#[Response(format(json))]
pub enum HelloResponse {
#[Response(code = 200)]
Hello(GreetMessage),
#[Response(code = 400)]
BadRequest(ErrorMessage),
}
#[DTO(response)]
pub struct GreetMessage {
message: String,
}
#[DTO(response)]
pub struct ErrorMessage {
error: &'static str,
}
}
fn make_router() -> GroomRouterValid {
api::into_router()
.validate()
.expect("GroomRouter validation failed for quick-example")
}
pub fn make_axum_router() -> axum::Router {
make_router().to_axum_router()
}
fn make_openapi(r: &GroomRouterValid) -> utoipa::openapi::OpenApi {
#[derive(OpenApi)]
#[openapi(info(title = "My API", version = "0.1.0"))]
struct ApiDoc;
r.to_openapi(ApiDoc::openapi())
}A handler can also return Result<R, E>, where both R and E are #[Response()] types.
Each #[Controller] module generates a primary public function:
into_router() -> GroomRouter<S>— returns aGroomRouterfor composition (.merge()/.nest()), validation (.validate()), and terminal conversion (.to_axum_router()/.to_openapi()).
Import groom::extract::GroomExtractor and groom::response::Response inside the controller module so extractors and response types appear in the OpenAPI spec.
Groom adds a statically-typed HTTP layer on top of axum and utoipa:
- Compile-time-typed handlers — handler signatures describe request inputs and response outputs at compile time; generated wrappers handle serialization, so handlers return domain types instead of manually built
Responsebodies. - Automatic content negotiation — when a response type declares multiple formats (for example
jsonandhtml), Groom selects the matching serializer from the client'sAcceptheader. - OpenAPI derived from the same types that drive routing — the spec comes from the types handlers use, so code and documentation stay in sync.
- Supplements axum rather than replacing it — middleware,
Extension,State, and non-Groom routes remain ordinary axum. - One handler backs a JSON API and HTML views — useful for status pages, admin dashboards, or embedding reports without maintaining separate endpoints.
- Quickstart — get a Groom API running in minutes
- User guide — annotations and how-tos
- API reference — GroomRouter, OpenApiSpecLayer, content negotiation
- Architecture — runtime + codegen internals
| Example | Path | Purpose |
|---|---|---|
| Quick example | examples/quick-example | JSON greet endpoint from this guide; snippet kept in sync with src/quickstart_snippet.rs. |
| Hello world | examples/hello-world | Single controller, plain-text responses, inline spec route. |
| HTMX app | examples/htmx | Simple backend with HTMX, rendered with the minijinja templating engine. |
| Todo app | examples/todo | Layered backend, multiple endpoints, spec binary, Vue frontend with generated client. |
Run the todo backend (with spec endpoint and CORS for local frontend):
cd examples/todo && just run-backendMIT or Apache 2.0.