A [`Router`] matches requests to handlers. Create one with [`Router::builder`], register handlers, and call [`build`](RouterBuilder::build). Pass the finished router to [`start`](crate::start) to serve requests.
Register handlers explicitly on the builder or enable the `discover` feature to collect annotated handlers automatically. Use [`module_router!`] to derive route paths from your module tree.
# Paths
Explicit route paths use Topcoat's [`Path`] syntax. A path is made of `/`-separated segments, and each segment is one of four kinds.
- `/users` is a static segment.
- `/{id}` is a dynamic parameter that matches one non-empty segment.
- `/{*path}` is a catch-all parameter that matches one or more remaining segments.
- `/(marketing)` is a group. Groups take part in layout and layer matching but are stripped from the served URL.
The root path is `/`. Every other path starts with `/` and has no empty segments, except that it may end in a `/`. Parameter and group names start with an ASCII letter or `_` and contain only ASCII letters, digits, and underscores.
A trailing slash is part of the path. A page at `/users/` is served at `/users/`, and a page at `/users` is served at `/users`. A request for the other form is redirected to the declared one by default. Use [`RouterBuilder::trailing_slash`] to configure this behavior.
# Pages
A page is an async function annotated with [`#[page]`](page) and a path, returning a rendered view:
```rust
use topcoat::{Result, router::page, view::{View, view}};
#[page("/")]
async fn home() -> Result<impl View> {
Ok(view! { <h1>"Home"</h1> })
}
#[page("/users/{id}")]
async fn user_profile() -> Result<impl View> {
Ok(view! { <h1>"User profile"</h1> })
}
```
A page serves `GET` by default; naming methods before the path (e.g., `#[page(POST "/signup")]`) overrides that, with the same method forms as [`#[route]`](macro@route).
See [`#[page]`](page) for the handler signature, module-derived paths, and using pages as components.
# Layouts
A layout wraps pages. It receives the inner page (or nested layout) as a [`Slot`], to embed in its own view. Annotate it with [`#[layout]`](layout):
```rust
use topcoat::{
Result,
router::{Slot, layout},
view::{View, view},
};
#[layout("/")]
async fn root_layout(slot: Slot<'_>) -> Result<impl View> {
Ok(view! {
<!DOCTYPE html>
<html>
<body>
<nav>
<a href="/">"Home"</a>
<a href="/about">"About"</a>
</nav>
(slot)
</body>
</html>
})
}
```
A layout matches a page by path segments. A layout at `/` wraps all pages. One at `/settings` wraps `/settings` and pages below it, such as `/settings/profile`. Matching layouts nest with the least specific path outside the most specific one. See [`#[layout]`](layout) for details.
# Layers
A layer wraps request handling under its path prefix. It receives the request context, the request body, and [`Next`], which represents the remaining layers and the handler. A layer that registers request-scoped values derives a child context with [`Cx::with`](crate::context::Cx::with) and passes that to `next.run`:
```rust
use topcoat::{
Result,
context::Cx,
router::{Body, Next, layer, response::Response},
};
#[layer("/")]
async fn timing(cx: &Cx, body: Body, next: Next<'_>) -> Result<Response> {
let start = std::time::Instant::now();
let response = next.run(cx, body).await?;
println!("handled in {:?}", start.elapsed());
Ok(response)
}
```
Layers follow the same prefix rule as layouts and nest from least specific (outermost) to most specific (innermost). A layer whose [`path`](Layer::path) is `None` wraps every request, including one that matches no route: the 404 or 405 error comes back through it as the `Err` returned by `next.run`. See [`#[layer]`](layer) for the exact matching and ordering rules.
# API routes
An API route is an async function annotated with [`#[route]`](macro@route) and an explicit HTTP method:
```rust
use topcoat::{Result, router::route};
#[route(GET "/api/health")]
async fn health() -> Result<&'static str> {
Ok("ok")
}
```
The method can also be a bracketed list (`#[route([GET, POST] "/form")]`) registering the handler for each listed method, or `*` (`#[route(* "/webhook")]`) registering it for every method. A route declaring a specific method takes precedence over a `*` route at the same path.
See [`#[route]`](macro@route) for the handler signature and how return values convert into responses.
# Request and response bodies
A page or route handler can take the request context as `cx: &`[`Cx`](crate::context::Cx) and, alongside it, a single request body parameter, such as a [`Json`](content::Json) body. An API route additionally returns a value that becomes the response.
```rust
# #[derive(serde::Deserialize)] struct CreateUser { name: String }
# #[derive(serde::Serialize)] struct User { name: String }
use topcoat::{
Result,
context::Cx,
router::{content::Json, route},
};
#[route(POST "/api/users")]
async fn create_user(cx: &Cx, Json(input): Json<CreateUser>) -> Result<Json<User>> {
// ...
# Ok(Json(User { name: input.name }))
}
```
Both parameters are optional and may appear in either order. A handler can take only one body parameter because the body can be consumed only once. Pages parse bodies the same way and return a view. See [`content`](mod@content) for request extractors and response types.
# Path and query parameters
Read path and query values from [`Cx`](crate::context::Cx). Any helper with access to the context can read them.
## Path parameters
Call [`path_param!`](macro@path_param) with the parameter name from the URL. The macro generates a Pascal-cased type, so `path_param!(post_id: u64)` declares `PostId` for `{post_id}`:
- After `path_param!(slug)`, `path_param::<Slug>(cx)` returns the percent-decoded segment as `&str`.
- A type after `:` is parsed with [`FromStr`](std::str::FromStr). The default return type is `Result<&T, &<T as FromStr>::Err>`.
- An `error = ...` option maps parse failures to a router error. See [`path_param!`](macro@path_param) for the supported forms.
```rust
use topcoat::{
Result,
context::Cx,
router::{page, path_param},
view::{View, view},
};
path_param!(post_id: u64, error = bad_request);
#[page("/posts/{post_id}")]
async fn post(cx: &Cx) -> Result<impl View> {
let post_id = path_param::<PostId>(cx)?;
Ok(view! { <h1>"Post " (post_id)</h1> })
}
```
Parsing occurs once per request and the result is memoized.
Prefix the name with `*` to capture the remaining path as decoded segments. After `path_param!(*doc_path)`, `path_param::<DocPath>(cx)` returns [`CatchAllSegments`]. After `path_param!(*ids: u32)`, `path_param::<Ids>(cx)` returns `Result<&[u32], _>`.
With [`module_router!`], a declaration inside a non-root route module also changes that module's segment to the parameter. See [`module_router!`] for module structure, nested parameters, and catch-all parameters.
## Query parameters
Apply [`#[query_params]`](macro@query_params) to a struct with named fields. The macro derives `serde::Deserialize`, and [`query_params::<T>(cx)`](fn@query_params) deserializes the request query string into that struct. Use `Option<T>` for keys that may be absent.
```rust
use topcoat::{
Result,
context::Cx,
router::{page, query_params},
view::{View, view},
};
#[query_params(error = bad_request)]
struct PostsQuery {
page: Option<u32>,
q: Option<String>,
}
#[page("/posts")]
async fn posts(cx: &Cx) -> Result<impl View> {
let query = query_params::<PostsQuery>(cx)?;
Ok(view! {
<p>"page: " (query.page.unwrap_or(1))</p>
<p>"search: " (query.q.as_deref().unwrap_or(""))</p>
})
}
```
Query parsing also occurs once per request and returns a reference to the memoized value. A query struct is independent of the matched route, so any handler with `cx: &Cx` can read it. See [`#[query_params]`](macro@query_params) for error handling and redirecting after invalid input.
# Errors
Handlers return a [`Result`](crate::Result). The router converts an unhandled error into an HTTP response. Router errors select a status code, while other errors produce `500 Internal Server Error`.
The [`error`](mod@error) module has a constructor for each response, like [`not_found()`](error::not_found) or [`redirect(uri)`](error::redirect), and the [`RouterErrorExt`](error::RouterErrorExt) methods that turn an `Option` or `Result` into one:
```rust
# use topcoat::{Result, context::Cx, router::{error::RouterErrorExt, page}, view::{View, view}};
# struct User;
# async fn current_session(_cx: &Cx) -> Option<User> { None }
#[page("/dashboard")]
async fn dashboard(cx: &Cx) -> Result<impl View> {
let _user = current_session(cx).await.ok_or_unauthorized()?;
Ok(view! { <h1>"Dashboard"</h1> })
}
```
See the [`error`](mod@error) module docs for how to raise, convert, and catch these errors.
# Status codes and headers
A [`StatusCode`] in a `view!`'s body sets the response status, and a [`HeaderMap`] or a single `(HeaderName, HeaderValue)` pair adds response headers. This pairs with error handling. Wrapping the slot in an [`error_boundary`](crate::view::error_boundary) lets a layout catch a page's [`NotFoundError`](error::NotFoundError) and replace it with a branded not-found page:
```rust
use topcoat::{
Result,
context::Cx,
router::{
Slot, StatusCode,
error::{NotFoundError, RouterErrorExt},
layout, page,
},
view::{View, error_boundary, view},
};
# struct Post { title: String }
# async fn find_post(_cx: &Cx) -> Option<Post> { None }
#[page("/posts/{id}")]
async fn post(cx: &Cx) -> Result<impl View> {
let post = find_post(cx).await.ok_or_not_found()?;
Ok(view! { <h1>(post.title)</h1> })
}
#[layout("/")]
async fn root_layout(slot: Slot<'_>) -> Result<impl View> {
Ok(view! {
<html>
<body>
error_boundary(
fallback: |error| {
if error.downcast_ref::<NotFoundError>().is_none() {
// Any other error type is rethrown.
return Err(error);
}
Ok(view! {
(StatusCode::NOT_FOUND)
<h1>"Page not found"</h1>
})
},
(slot)
)
</body>
</html>
})
}
```
See the [`view!`](crate::view::view!) macro docs for the full placement and precedence rules, and the [`error`](mod@error) module docs for catching errors.
# Cross-origin requests
The router applies an [`OriginPolicy`] to every request before any layer or handler runs: by default, state-changing cross-origin browser requests and cross-origin WebSocket handshakes are rejected with `403 Forbidden`. Register a policy with [`origin_policy`](RouterBuilder::origin_policy) to trust cross-origin peers, to exempt individual routes, or to opt out; see [`OriginPolicy`] for the exact rules.
# Manual registration
Build a router by chaining `.page()`, `.layout()`, `.layer()`, and `.route()`, then calling [`build`](RouterBuilder::build):
```rust
# use topcoat::{Result, context::Cx, router::{Body, Next, Slot, layer, layout, page, response::Response, route}, view::{View, view}};
# #[layout("/")] async fn root_layout(slot: Slot<'_>) -> Result<impl View> { Ok(view! { (slot) }) }
# #[layout("/settings")] async fn settings_layout(slot: Slot<'_>) -> Result<impl View> { Ok(view! { (slot) }) }
# #[layer("/")] async fn timing(cx: &Cx, body: Body, next: Next<'_>) -> Result<Response> { next.run(cx, body).await }
# #[page("/")] async fn home() -> Result<impl View> { Ok(view! { <h1>"Home"</h1> }) }
# #[page("/about")] async fn about() -> Result<impl View> { Ok(view! { <h1>"About"</h1> }) }
# #[page("/settings/profile")] async fn profile() -> Result<impl View> { Ok(view! { <h1>"Profile"</h1> }) }
# #[route(GET "/api/health")] async fn health() -> Result<&'static str> { Ok("ok") }
use topcoat::router::Router;
pub fn router() -> Router {
Router::builder()
.layout(root_layout)
.layout(settings_layout)
.layer(timing)
.page(home)
.page(about)
.page(profile)
.route(health)
.build()
}
```
Layout and layer matching is based on path prefixes, not registration order; see [`#[layout]`](layout) and [`#[layer]`](layer) for the ordering rules.
# Auto-discovery with `discover()`
With the `discover` feature enabled, every [`#[page]`](page), [`#[layout]`](layout), [`#[layer]`](layer), and [`#[route]`](macro@route) is collected at link time. Instead of listing each item by hand, call [`discover`](RouterBuilderDiscoverExt::discover) on the builder:
```rust
use topcoat::router::{Router, RouterBuilderDiscoverExt};
pub fn router() -> Router {
Router::builder().discover().build()
}
```
This finds annotated items across your crate and dependencies. Discovered layers must have unique paths because link-time collection order is not stable; if you need to stack several layers on one path, register them explicitly with `.layer(...)`.
Discovery also registers items collected by enabled integrations. It does not replace their setup steps. Follow each integration's guide to register any configuration or services it requires.
[`module_router!`] registers module-derived handlers only. It returns a `RouterBuilder`, so call `discover()` on it, or register the remaining items by hand, exactly as above.
# Static files
For application assets such as images and stylesheets, we recommend the [asset system](../asset/index.html). It generates content-hashed URLs for long-lived caching, and Rust asset handles help avoid typos in URL strings. Serving a directory is useful when files need fixed URLs or are created at runtime.
Enable the `fs` feature to serve files from a directory. Import [`RouterBuilderDirectoryExt`] and call [`public_dir`](RouterBuilderDirectoryExt::public_dir) to serve them at the site root:
```rust
# #[cfg(feature = "fs")]
# {
use topcoat::router::{Router, RouterBuilderDirectoryExt};
let router = Router::builder().public_dir("./public").build();
# }
```
This serves `./public/logo.svg` at `/logo.svg` and `./public/css/site.css` at `/css/site.css`. The directory is relative to the process's current working directory. The root URL `/` is left available for a home page.
Use [`serve_dir`](RouterBuilderDirectoryExt::serve_dir) to choose a different route path. Its final catch-all parameter selects the file within the directory:
```rust
# #[cfg(feature = "fs")]
# {
use topcoat::router::{Router, RouterBuilderDirectoryExt};
let router = Router::builder()
.serve_dir("/downloads/{*file}", "./files")
.build();
# }
```
This serves `./files/report.pdf` at `/downloads/report.pdf`. The catch-all does not match `/downloads` or `/downloads/` itself.
# Serving
Use [`start`](crate::start) to run a finalized router:
```rust,no_run
# mod my_app {
# use topcoat::router::{Router, RouterBuilderDiscoverExt};
# pub fn router() -> Router { Router::builder().discover().build() }
# }
#[tokio::main]
async fn main() {
let router = my_app::router();
topcoat::start(router).await.unwrap();
}
```
[`start`](crate::start) reads `HOST` and `PORT`, with `127.0.0.1:3000` as the default. Use [`serve`](crate::serve) to supply your own [`Listener`]. On Unix, a `UnixListener` can accept requests forwarded by a reverse proxy:
```rust,no_run
# #[cfg(unix)]
# async fn serve(router: topcoat::router::Router) -> std::io::Result<()> {
let path = "/run/my-app.sock";
let _ = std::fs::remove_file(path);
let listener = tokio::net::UnixListener::bind(path)?;
topcoat::serve(listener, router).await
# }
```
The socket file of a previous run is not removed automatically, so remove any stale file before binding, as above.
The HTTP server requires the `serve` feature, which is enabled by default. When another platform receives requests for you, call [`Router::handle`] to turn a [`Request`](request::Request) into a [`Response`](response::Response). This works without `serve`.
# Tower services
With the `tower` feature enabled, the [`tower`](mod@tower) module bridges the tower ecosystem: [`TowerRoute`](tower::TowerRoute) mounts a tower service (like an axum router) as a route, and [`TowerLayer`](tower::TowerLayer) runs tower middleware as a layer. See the [`tower`](mod@tower) module docs for details.
# Example: full manual setup
```rust
use topcoat::{
Result,
context::Cx,
router::{
Body, Next, Router, Slot, content::Json, layer, layout, page, response::Response, route,
},
view::{View, view},
};
#[derive(serde::Deserialize, serde::Serialize)]
struct NewUser {
name: String,
}
#[layout("/")]
async fn root_layout(slot: Slot<'_>) -> Result<impl View> {
Ok(view! {
<!DOCTYPE html>
<html>
<body>
<nav>
<a href="/">"Home"</a>
<a href="/users">"Users"</a>
</nav>
(slot)
</body>
</html>
})
}
#[layer("/api")]
async fn api_log(cx: &Cx, body: Body, next: Next<'_>) -> Result<Response> {
let response = next.run(cx, body).await?;
println!("API response: {}", response.status());
Ok(response)
}
#[page("/")]
async fn home() -> Result<impl View> {
Ok(view! { <h1>"Welcome"</h1> })
}
#[page("/users")]
async fn users_list() -> Result<impl View> {
Ok(view! { <h1>"All users"</h1> })
}
#[page("/users/{id}")]
async fn user_profile() -> Result<impl View> {
Ok(view! { <h1>"User profile"</h1> })
}
#[route(GET "/api/health")]
async fn health() -> Result<&'static str> {
Ok("ok")
}
// Reads a JSON request body and echoes it back as a JSON response.
#[route(POST "/api/users")]
async fn create_user(Json(user): Json<NewUser>) -> Result<Json<NewUser>> {
Ok(Json(user))
}
pub fn router() -> Router {
Router::builder()
.layout(root_layout)
.layer(api_log)
.page(home)
.page(users_list)
.page(user_profile)
.route(health)
.route(create_user)
.build()
}
```
# Example: same app with `discover()`
```rust
use topcoat::router::{Router, RouterBuilderDiscoverExt};
// The page, layout, layer, and route definitions are identical. Only the
// router function changes.
pub fn router() -> Router {
Router::builder().discover().build()
}
```
All [`#[page]`](page), [`#[layout]`](layout), [`#[layer]`](layer), and [`#[route]`](macro@route) items from the example above are picked up automatically.