Routing
Routing
Blixt uses Axum for routing. The prelude re-exports everything you need: Router, get, post, put, delete, plus extractors like Path, Query, and State.
use blixt::prelude::*;
Defining routes
Register routes with Axum's Router::new().route() method. Each route takes a path pattern and a method handler.
fn routes() -> Router<AppContext> {
Router::new()
.route("/", get(index))
.route("/posts", get(list).post(create))
.route("/posts/{id}", get(show).put(update).delete(remove))
}
Path parameters use {name} syntax. Multiple HTTP methods can be chained on the same path.
Handlers
A handler is an async function that returns Result<impl IntoResponse>. Use the render! macro for HTML responses.
async fn index() -> Result<impl IntoResponse> {
render!(HomePage { title: "Welcome".to_string() })
}
Path parameters
Extract path segments with Path:
async fn show(Path(id): Path<i64>) -> Result<impl IntoResponse> {
// id is extracted from /posts/{id}
render!(ShowPage { id })
}
For multiple parameters, destructure into a tuple:
async fn comment(
Path((post_id, comment_id)): Path<(i64, i64)>,
) -> Result<impl IntoResponse> {
// from /posts/{post_id}/comments/{comment_id}
todo!()
}
Query parameters
Extract query strings with Query and a Deserialize struct:
#[derive(Deserialize)]
struct Filters {
status: Option<String>,
sort: Option<String>,
}
async fn list(Query(filters): Query<Filters>) -> Result<impl IntoResponse> {
// /posts?status=published&sort=newest
todo!()
}
For pagination, Blixt provides PaginationParams as a built-in extractor that reads page and per_page from the query string:
async fn list(pagination: PaginationParams) -> Result<impl IntoResponse> {
// pagination.page() defaults to 1
// pagination.per_page() defaults to 25, clamped to 1..=100
todo!()
}
State extraction
Share application state across handlers with State. Blixt provides AppContext which holds the database pool and configuration:
async fn index(State(ctx): State<AppContext>) -> Result<impl IntoResponse> {
let posts = Select::from("posts")
.columns(&["id", "title"])
.fetch_all::<Post>(&ctx.db)
.await?;
render!(IndexPage { posts })
}
Create the context during startup:
let config = Config::from_env()?;
let pool = blixt::db::create_pool(&config).await?;
let ctx = AppContext::new(pool, config.clone());
Route groups
Factor routes into separate functions per resource:
fn post_routes() -> Router<AppContext> {
Router::new()
.route("/posts", get(list).post(create))
.route("/posts/{id}", get(show).put(update).delete(remove))
}
fn comment_routes() -> Router<AppContext> {
Router::new()
.route("/posts/{post_id}/comments", get(list_comments).post(add_comment))
}
Merge them into a single router:
fn routes() -> Router<AppContext> {
post_routes().merge(comment_routes())
}
The App builder
App assembles your routes with the middleware stack and starts the server. The builder has three methods:
| Method | Purpose |
|---|---|
.router(router) | Sets the application router with user-defined routes |
.static_dir(path) | Serves static files from path at /static/ |
.serve() | Binds to host:port from config and starts accepting connections |
#[tokio::main]
async fn main() -> Result<()> {
init_tracing()?;
let config = Config::from_env()?;
let pool = blixt::db::create_pool(&config).await?;
blixt::db::migrate(&pool).await?;
let ctx = AppContext::new(pool, config.clone());
App::new(config)
.router(routes().with_state(ctx))
.static_dir("static")
.serve()
.await
}
When you call .with_state(ctx) on your router, Axum makes the context available to all handlers via State<AppContext>.
Built-in middleware
App applies middleware in this order (outermost first):
- Tracing -- request/response logging
- Request ID -- adds a unique ID to each request
- Security headers -- CSP, HSTS, X-Frame-Options, etc.
- Compression -- gzip/brotli response compression
Static files served via .static_dir() also get immutable cache headers (Cache-Control: public, max-age=31536000, immutable) and dotfile/path-traversal protection.
Minimal example
A complete application without a database:
use blixt::prelude::*;
#[derive(Template)]
#[template(path = "pages/home.html")]
struct HomePage {
greeting: String,
}
async fn index() -> Result<impl IntoResponse> {
render!(HomePage {
greeting: "Hello from Blixt!".to_string(),
})
}
#[tokio::main]
async fn main() -> Result<()> {
init_tracing()?;
let config = Config::from_env()?;
App::new(config)
.router(Router::new().route("/", get(index)))
.static_dir("static")
.serve()
.await
}