# How to Use the module_router! Macro for Automatic Route Discovery in Topcoat

> Discover automatic route discovery with Topcoat's module_router! macro. Compile routes from your Rust module hierarchy to simplify web application development.

- Repository: [Tokio/topcoat](https://github.com/tokio-rs/topcoat)
- Tags: how-to-guide
- Published: 2026-07-31

---

**The `module_router!` macro in Topcoat builds a complete router at compile-time by traversing your Rust module hierarchy, automatically mapping each module to a URL path segment while letting handlers inherit the derived route prefix.**

The `module_router!` macro in the tokio-rs/topcoat repository eliminates boilerplate route registration by deriving your application's URL structure directly from your Rust module tree. This declarative approach ensures that your file system organization automatically determines your API surface, with each module contributing a path segment converted to kebab-case by default.

## Setting Up the module_router! Macro

Place the macro invocation at the root of your route tree where it can access the module hierarchy. The root module maps to `/`, and calling `module_router!()` returns a `RouterBuilder` that allows method chaining before finalizing with `.build()`.

The macro requires the `discover` feature, which the top-level `topcoat` crate enables by default. With this feature enabled, you can call `.discover()` on the builder to register items that are not module-derived, such as explicit-path handlers, fonts, and static assets.

```rust
// src/app.rs
pub fn router() -> topcoat::router::Router {
    topcoat::router::module_router!().build()
}

```

For a production setup that includes static assets and automatic discovery of explicit-path items, chain the discovery method before building:

```rust
use topcoat::{
    asset::{AssetBundle, RouterBuilderAssetExt},
    router::{Router, RouterBuilderDiscoverExt},
};

pub fn router() -> Router {
    topcoat::router::module_router!()
        .discover()                     // registers explicit-path handlers, fonts, etc.
        .assets(AssetBundle::load().unwrap())
        .build()
}

```

## How Module Hierarchy Maps to URL Paths

Each sub-module contributes exactly one path segment to the final route. Topcoat automatically converts module names to kebab-case for URL-friendly formatting.

| Module | Route Path |
|--------|------------|
| `app` | `/` |
| `app::about` | `/about` |
| `app::blog_posts` | `/blog-posts` |
| `app::settings::profile` | `/settings/profile` |

The function name inside a module does **not** affect the final path. If two handlers exist in the same module, they share the same base path derived from that module's name. According to the implementation in [`crates/topcoat-router/docs/module_router.md`](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat-router/docs/module_router.md), this design ensures that the module structure—not individual function names—determines your routing table.

## Dynamic Path Parameters and Catch-All Routes

Use the `path_param!` macro (or alternatively `segment!`) inside a module to convert its static segment into a dynamic parameter (`{param}`) or a catch-all wildcard (`{*param}`). This is implemented in [`crates/topcoat-router/src/module/router.rs`](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat-router/src/module/router.rs).

```rust
// src/app/posts/post_id.rs
use topcoat::{Result, context::Cx, router::{page, path_param}, view::view};

path_param!(post_id: u64, error = bad_request);

#[page]
async fn post(cx: &Cx) -> Result {
    let id = path_param::<PostId>(cx)?;
    view! { <h1>"Post " (id)</h1> }
}

```

The `path_param!` declaration defines the type and error handling for the parameter, which is then extracted from the request context in the handler. This allows the `post_id` module to map to routes like `/posts/123` while automatically validating the parameter type.

## Combining Automatic Discovery with Explicit Routes

Handlers that specify an explicit path string using `#[page("/legacy")]` or `#[route("/api/v1")]` **opt-out** of module-derived routing entirely. You can still register these handlers on the same builder either individually or by calling `.discover()` to scan for all attributed items that are not part of the module tree.

This architecture, detailed in [`crates/topcoat-router/docs/module_router.md`](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat-router/docs/module_router.md), allows you to mix convention-based routing (module hierarchy) with configuration-based routing (explicit paths) in the same application.

## Complete Working Example

The runtime demo in [`examples/runtime/src/main.rs`](https://github.com/tokio-rs/topcoat/blob/main/examples/runtime/src/main.rs) demonstrates a full implementation combining module-derived routes, static assets, and explicit discovery:

```rust
// examples/runtime/src/main.rs
#[tokio::main]
async fn main() {
    topcoat::start(
        module_router!()                // derive routes from the module tree
            .assets(AssetBundle::load().unwrap())
            .discover()                // add explicit-path handlers, assets, etc.
            .build(),
    )
    .await
    .unwrap();
}

```

Individual pages within the module hierarchy, such as the counter example in [`examples/runtime/src/counter.rs`](https://github.com/tokio-rs/topcoat/blob/main/examples/runtime/src/counter.rs), use the standard attributes without path arguments to inherit their parent's route:

```rust
#[layout]
async fn root_layout(slot: Result) -> Result {
    view! { <html><body>(slot?)</body></html> }
}

#[page]
async fn home() -> Result {
    view! { <h1>"Home"</h1> }
}

```

## Summary

- **Root placement**: Invoke `module_router!()` at your route tree root to return a `RouterBuilder` for chaining with `.build()`.
- **Automatic mapping**: Each module contributes one kebab-case path segment; handlers inherit this prefix via `#[page]`, `#[layout]`, `#[layer]`, or `#[route]` attributes.
- **Shared paths**: Multiple handlers in the same module share the identical base path, as the function name does not influence routing.
- **Dynamic segments**: Use `path_param!` to create parameterized routes like `/posts/{id}` with type-safe extraction.
- **Explicit opt-out**: Handlers with explicit path strings bypass module discovery but can be registered via `.discover()` alongside automatic routes.
- **Implementation details**: The macro logic resides in [`crates/topcoat-router/src/module/router.rs`](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat-router/src/module/router.rs), while discovery extensions are defined in [`crates/topcoat-router/src/router.rs`](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat-router/src/router.rs).

## Frequently Asked Questions

### Does module_router! work with API routes and layouts?

Yes. The `#[route]` attribute for API endpoints and the `#[layout]` attribute for nested UI layouts work identically to `#[page]`. They automatically inherit the module-derived path prefix. According to the source in [`crates/topcoat-router/docs/module_router.md`](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat-router/docs/module_router.md), these attributes participate in the same compile-time module walk that builds the route tree.

### What happens if I place two page handlers in the same module?

Both handlers will share the exact same base path derived from the module name. The function names themselves do not create distinct routes. To differentiate endpoints, either place handlers in separate sub-modules or use explicit path attributes like `#[page("/unique")]` to override the automatic routing for that specific handler.

### How do I opt out of automatic route discovery for specific handlers?

Apply an explicit path string to the attribute, such as `#[page("/legacy-path")]` or `#[route("/api/status")]`. As documented in `crates/topcoat-router/docs/module_router.md#explicit-paths`, this opts the handler out of module-derived routing. You must then register it manually or call `.discover()` on the `RouterBuilder` to include it in the final router.

### Where is the module_router! macro implemented?

The procedural macro implementation and the `RouterBuilder` return type are located in [`crates/topcoat-router/src/module/router.rs`](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat-router/src/module/router.rs). The discovery extensions that enable `.discover()` are implemented in [`crates/topcoat-router/src/router.rs`](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat-router/src/router.rs) as part of the `RouterBuilderDiscoverExt` trait.