---
name: leptos-routing
description: Configure Leptos router with typed paths, nested routes, params, and Axum server-side mounting. Auto-triggers when adding routes, reading path/query params, implementing nested layouts, or wiring leptos_routes_with_context.
---

# Leptos Routing

## When to Use

Invoke before:
- Adding any `<Route>` or `<ParentRoute>` to the router.
- Reading path parameters with `use_params` or query params with `use_query_map`.
- Implementing nested layouts with `<Outlet/>`.
- Mounting routes on the Axum server via `leptos_routes_with_context`.
- Adding programmatic navigation with `use_navigate`.

## Core Patterns

### Pattern 1 — Router Setup with Typed Paths

`path!("/x")` is the 0.7+ typed path macro. Use it for all route paths. `<Routes fallback=...>` wraps all routes and renders the fallback when no route matches.

```rust
use leptos::prelude::*;
use leptos_router::{components::*, path};

#[component]
pub fn App() -> impl IntoView {
    view! {
        <Router>
            <nav>
                <A href="/">"Home"</A>
                <A href="/posts">"Posts"</A>
            </nav>
            <main>
                <Routes fallback=|| view! { <NotFound /> }>
                    <Route path=path!("/") view=HomePage />
                    <Route path=path!("/posts") view=PostListPage />
                    <Route path=path!("/posts/:id") view=PostDetailPage />
                    <Route path=path!("/about") view=AboutPage />
                </Routes>
            </main>
        </Router>
    }
}
```

Anti-pattern — using string literals directly for `path=`:
```rust
// BAD: string path is 0.6 syntax; in 0.7+ use path!() macro for type safety
<Route path="/posts/:id" view=PostDetailPage />
// CORRECT:
<Route path=path!("/posts/:id") view=PostDetailPage />
```

### Pattern 2 — Path and Query Parameters

`use_params::<T>()` returns `Memo<Result<T, ParamsError>>`. Params struct must derive `Params`. `use_query_map()` returns all query params as a `Memo<ParamsMap>`.

```rust
use leptos::prelude::*;
use leptos_router::{hooks::*, params::Params};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Params, Deserialize)]
pub struct PostParams {
    pub id: Option<i64>,
}

#[derive(Debug, Clone, PartialEq, Params, Deserialize)]
pub struct SearchQuery {
    pub q: Option<String>,
    pub page: Option<u32>,
}

#[component]
pub fn PostDetailPage() -> impl IntoView {
    // Memo<Result<PostParams, _>> — recomputes when path segment changes
    let params = use_params::<PostParams>();
    let id = Memo::new(move |_| {
        params.get().ok().and_then(|p| p.id)
    });

    // Query params: /posts/1?q=foo&page=2
    let query = use_query_map();
    let search_term = Memo::new(move |_| {
        query.get().get("q").unwrap_or_default().to_owned()
    });

    let post = Resource::new(
        move || id.get(),
        |id| async move {
            match id {
                Some(id) => get_post(id).await,
                None => Err(AppError::NotFound),
            }
        },
    );

    view! {
        <Suspense fallback=|| view! { <p>"Loading..."</p> }>
            // ...
        </Suspense>
    }
}
```

Anti-pattern — reading params inside an `Effect`:
```rust
// BAD: params are reactive; reading in Effect doesn't trigger view updates
Effect::new(move |_| {
    let params = use_params::<PostParams>().get();
    // ...
});
// CORRECT: use Memo or read directly in a reactive closure inside view!
```

### Pattern 3 — Nested Routes with Outlet

Parent routes render a layout; children fill the `<Outlet/>`. Each level of nesting is a `<ParentRoute>` containing child `<Route>` elements.

```rust
#[component]
pub fn SettingsLayout() -> impl IntoView {
    view! {
        <div class="settings">
            <nav class="settings-nav">
                <A href="/settings/profile">"Profile"</A>
                <A href="/settings/security">"Security"</A>
                <A href="/settings/billing">"Billing"</A>
            </nav>
            <div class="settings-content">
                <Outlet />  // ← child route renders here
            </div>
        </div>
    }
}

// In App component:
view! {
    <Router>
        <Routes fallback=|| view! { <NotFound /> }>
            <ParentRoute path=path!("/settings") view=SettingsLayout>
                <Route path=path!("/profile") view=ProfilePage />
                <Route path=path!("/security") view=SecurityPage />
                <Route path=path!("/billing") view=BillingPage />
                // Index route for /settings itself:
                <Route path=path!("") view=SettingsIndexPage />
            </ParentRoute>
        </Routes>
    </Router>
}
```

Anti-pattern — omitting `<Outlet />` in a parent route view:
```rust
// BAD: without <Outlet/>, child route components never render
#[component]
pub fn DashboardLayout() -> impl IntoView {
    view! {
        <div class="dashboard">
            <Sidebar />
            // ← missing <Outlet /> — child routes are invisible
        </div>
    }
}
```

### Pattern 4 — Axum Server Mounting

Generate the route list from the Leptos `App` component, then register it with Axum. Use `leptos_routes_with_context` to inject state into the Leptos server-side render context.

```rust
// src/main.rs (SSR binary)
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};

#[tokio::main]
async fn main() {
    let conf = get_configuration(None).unwrap();
    let leptos_options = conf.leptos_options.clone();
    let addr = leptos_options.site_addr;

    let pool = PgPool::connect(&std::env::var("DATABASE_URL").unwrap()).await.unwrap();
    let pool_clone = pool.clone();

    // Generate route list from App component (must match client-side routes exactly)
    let routes = generate_route_list(App);

    let router = axum::Router::new()
        // Inject DB pool and auth state into Leptos context on every request
        .leptos_routes_with_context(
            &leptos_options,
            routes,
            move || {
                provide_context(pool_clone.clone());
                provide_context(AuthState::new());
            },
            |opts| shell(opts),
        )
        .fallback(leptos_axum::file_and_error_handler(shell))
        .with_state(leptos_options);

    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
    axum::serve(listener, router).await.unwrap();
}
```

Anti-pattern — providing context via Axum `Extension` instead of `leptos_routes_with_context`:
```rust
// BAD: Extension state is NOT visible to #[server] fns via expect_context()
router.layer(Extension(pool))  // ← server fns can't see this
// CORRECT: provide via leptos_routes_with_context closure
```

### Pattern 5 — Programmatic Navigation and Links

`use_navigate()` returns a callable for programmatic navigation. `<A href="/x">` intercepts clicks and uses client-side nav (no full-page reload).

```rust
// LoginForm is #[island] — it owns ServerAction, Effect, and an interactive submit button.
// As a #[component] the submit handler would never attach (see [[leptos-islands]]).
#[island]
pub fn LoginForm() -> impl IntoView {
    let login = ServerAction::<Login>::new();
    let navigate = use_navigate();

    // Navigate after successful login
    Effect::new(move |_| {
        if let Some(Ok(_)) = login.value().get() {
            navigate("/dashboard", Default::default());
        }
    });

    view! {
        <ActionForm action=login>
            <input name="username" type="text" required />
            <input name="password" type="password" required />
            <button type="submit" disabled=move || login.pending().get()>"Login"</button>
        </ActionForm>
    }
}

// Typed link: uses islands-aware client navigation (re-fetches HTML, re-hydrates islands),
// no full reload.
view! {
    <A href="/posts/42">"Read post"</A>
}
```

Anti-pattern — using `<a href="...">` HTML element instead of `<A href=...>`:
```rust
// BAD: plain <a> causes a full page reload; the islands-aware router never sees it
view! { <a href="/posts">"Posts"</a> }
// CORRECT: use <A href="/posts"> — islands-router fetches new SSR HTML and re-hydrates islands
```

## Anti-Patterns to Block

- `path="/x"` string literal for route paths → use `path=path!("/x")` macro.
- `<a href=...>` for internal links → use `<A href=...>` (capital A — islands-router does in-app navigation by re-fetching SSR HTML and re-hydrating islands, not a full reload).
- Missing `<Outlet />` in parent route view component → child routes are never rendered.
- Reading params inside `Effect::new` → read via `use_params()` in reactive closures or `Memo`.
- Providing app state via Axum `Extension` layer instead of `leptos_routes_with_context` → server fns cannot access it via `expect_context()`.
- Calling `use_navigate()` during SSR (outside `Effect`) → panics server-side; always wrap in `Effect::new`.
- Route list in Axum not matching `App` component routes → 404s or missing SSR for some pages.

## Verification Hooks

```bash
# Detect old-style string path= on Route (missing path! macro)
grep -rn 'path="/' src/

# Detect plain <a href for internal navigation
grep -rn '<a href="/' src/ | grep -v 'noopener\|external\|// @external'

# Detect use_navigate called outside Effect (SSR panic risk)
grep -rn 'use_navigate' src/ | grep -v 'Effect\|#\[cfg\|//\|let navigate'

# Confirm leptos_routes_with_context is used (not leptos_routes)
grep -rn 'leptos_routes' src/ | grep -v 'with_context'

# Confirm Outlet is present in every ParentRoute view component
grep -rn 'ParentRoute' src/ | while read f; do
  comp=$(echo "$f" | grep -oP 'view=\K\w+');
  grep -rn "Outlet" src/ | grep -q "$comp" || echo "MISSING Outlet in $comp";
done
```

## References

- https://docs.rs/leptos_router/latest/leptos_router/ (full API)
- https://book.leptos.dev/router/
- https://docs.rs/leptos_axum/latest/leptos_axum/fn.generate_route_list.html
- https://docs.rs/leptos_axum/latest/leptos_axum/trait.LeptosRoutes.html
