Routes
Declare your app's URL patterns once with the Routes builder - handlers and the dev-mode 404 panel stay in sync without parallel declarations.
Routes is umbral's URL-patterns builder. Each .get(...), .post(...), etc. call registers the handler with the axum router AND records the path in the framework's route registry in the same step. The dev-mode 404 page reads that registry, so the URL list it shows always matches what's actually wired in. No parallel path_list to drift out of sync.
The basic shape
use umbral::prelude::*; App::builder() .routes( Routes::new() .get("/", home) .get("/articles", list_articles_html) .get("/articles/{id}", article_detail) .post("/api/articles", create_article) .delete("/api/articles/{id}", destroy_article), ) .build()?;That single block does two things at once: every method call adds the handler to axum's router and pushes a RouteSpec { path, methods: ["GET"] } into the registry the dev 404 panel renders.
Per-method shorthand
One method per call, with the framework wrapping your handler in the right axum::routing::<method>(...):
| Method | Routes call |
|---|---|
| GET | .get(path, handler) |
| POST | .post(path, handler) |
| PUT | .put(path, handler) |
| PATCH | .patch(path, handler) |
| DELETE | .delete(path, handler) |
| HEAD | .head(path, handler) |
| OPTIONS | .options(path, handler) |
handler is any axum Handler<T, ()> - async fn returning anything that IntoResponses.
Per-route middleware: .layered
When you need middleware (auth gating, rate-limiting, per-route timeouts) on one route only, build a MethodRouter with .layer(...) and pass it via .layered(method, path, mr):
use axum::routing::get;use umbral_auth::login_required::login_required_html; Routes::new() .get("/", home) .layered( "GET", "/dashboard", get(dashboard).layer(login_required_html("/login")), )The gotcha .layered protects you from: axum::Router::new().route(...).layer(L) applies L to every route on that Router instance. MethodRouter::layer(L) scopes to just that path. Routes::layered accepts a MethodRouter so layers attach where you expect.
Multi-method on one path: .route
When two methods share the same path (e.g. a collection endpoint accepting both GET and POST):
use axum::routing::{get, post}; Routes::new().route( &["GET", "POST"], "/api/comments", get(list_comments).post(create_comment),)The methods slice is what shows up as method badges on the dev 404 panel; the chained MethodRouter is what axum dispatches against.
Escape hatch for axum power-users: .with_router
For features the per-method shorthands don't expose - typed State, nest, fallback, custom middleware stacks - build a plain axum::Router and merge it in:
use axum::Router;use axum::routing::get; Routes::new() .get("/", home) .with_router( Router::new() .nest("/api", api_router()) .fallback(custom_404_json), )Paths inside the external router contribute their handlers but don't appear in the framework's route registry - axum doesn't expose its internal route table, so the dev 404 panel can't see them. To surface them, declare the same paths through the per-method shorthands on a Routes builder (or a plugin's route_paths()) so they land in the registry.
See also
- Auth gating - the
login_required_*layers commonly used with.layered - Trailing slash -
SlashRedirectpolicy - Error pages -
not_found_template/server_error_template(the dev 404 panel lives in the default 404 template)