Skip to content
Open source · MIT · net8.0 / net9.0 / net10.0

Routing for Blazor,
done right.

A modern, declarative, nestable router with everything today's router libraries are measured by - async guards, data loaders with stale-while-revalidate caching, keep-alive, view transitions and compile-time-safe URLs - while your existing @page components keep working unchanged.

# the router
dotnet add package Bit.Brouter
# optional: typed, compile-time-safe URL builders
dotnet add package Bit.Brouter.Generators
localhost:5000/users/42/orders
@* AppRouter.razor - routes are components, so nesting is just markup *@
<Brouter NotFoundUrl="404" AppAssembly="@GetType().Assembly">
    <Broute Path="/" Component="@typeof(HomePage)" />

    <Broute Path="/users/{id:int}"
            Guard="@RequireSignIn"
            Loader="@LoadUser"
            StaleTime="@TimeSpan.FromMinutes(1)">
        <Content><UserPage /></Content>
        <Routes>
            <Broute Path="/orders" Component="@typeof(OrdersPage)" />
        </Routes>
    </Broute>
</Brouter>
Framework parityevery @page template matches identically
Prerender-readyServer, WebAssembly & Auto render modes
Zero JS setupscripts ship as static web assets
Fails closedauthorization stays Microsoft's code

Capabilities

Everything a modern router does - in Blazor

API design informed by React Router, Vue Router, Angular Router, SvelteKit and TanStack Router - built on ASP.NET Core's own route-template semantics.

Full template grammar

Literals, typed parameters, optionals, defaults, complex segments ({name}.{ext?}) and catch-alls - a superset-compatible match of the built-in router, with specificity-based conflict resolution.

Route templates →

Nested routes & outlets

Parents render persistent layout and children fill outlets - including several named regions per route, and pathless groups for shared guards and loaders.

Nested routes →

Guards & navigation locks

Async enter and leave guards that cancel preventively - the URL never flickers - plus component-level locks that hold a navigation open for a custom "unsaved changes" dialog.

Guards →

Data loaders & caching

Fetch before render, with stale-while-revalidate caching, hover and viewport preloading, streamed deferred data, revalidation after mutations, and error boundaries with retry.

Data loading →

View transitions

Direction-aware page animations and shared-element morphs through the browser's View Transitions API - one option to enable, overridable with plain CSS.

View transitions →

Keep-alive & lifecycle

Keep pages mounted - per route or per parameter, LRU-bounded - so Back restores exact state, with activate/deactivate/renavigate callbacks to pause and resume background work.

Lifecycle →

Typed routes

A source generator turns your route declarations into compile-time-safe URL builders - BrouterRoutes.Counter(42) instead of a string, so a broken link becomes a build error.

Typed routes →

Awaitable navigation

NavigateAsync tells you how a navigation actually ended - succeeded, cancelled, redirected, not found, failed or superseded - plus history entry state and functional query updates.

Navigation →

Drop-in migration

@page discovery, the Found template, zero-template authorization and layouts all keep the built-in Router's behavior - migrate first, adopt features incrementally.

Migration →

In practice

See it in code

01

Guard it - preventively

Guards run before the URL commits. Cancel or redirect and the address bar never moves - no flicker, no corrupted Back button, and real "unsaved changes" prompts become possible.

Guards & locks →
<Broute Path="/admin" Guard="@CheckAdmin">…</Broute>

private async ValueTask CheckAdmin(BrouterNavigationContext ctx)
{
    if (await auth.IsAdminAsync(ctx.CancellationToken) is false)
        ctx.Redirect("/login");   // URL never committed
}
02

Load data before render

A route's loader runs after guards pass, and its result cascades to the page as a typed wrapper. Add a StaleTime and Back/Forward becomes instant; add Preload on a link and the data is ready before the click.

Data loading →
<Broute Path="/users/{id:int}" Loader="@LoadUser"
        StaleTime="@TimeSpan.FromMinutes(1)">…</Broute>

<BrouterLink Href="/users/42" Preload="BrouterLinkPreload.Intent">
    Saleh
</BrouterLink>
03

Never ship a broken link

The optional source generator reads your @page directives and <Broute> declarations and emits typed URL builders - constraint types included, so {id:int} becomes an int argument.

Typed routes →
@* generated from: <Broute Name="counter" Path="/counter/{init:int}" /> *@
<BrouterLink Href="@BrouterRoutes.Counter(1234)">Counter</BrouterLink>

brouter.Navigate(BrouterRoutes.ProfileByUsername("saleh", query: "tab=posts"));

Comparison

Why not the built-in Router?

Because the built-in Router stops at matching. Everything it does, Brouter does the same way - and then keeps going. Where the framework's implementation is the right one - authorization, layouts - Brouter composes it instead of replacing it.

CapabilityBuilt-in RouterBrouter
Route templates & constraintsFull grammarSame grammar, identical matching semantics
Nested routes & outlets-Route trees, outlets, named views, pathless groups
Navigation guards-Async enter/leave guards + component-level locks, all preventive
Route data loadingPer-component OnParametersSetAsyncLoaders + SWR cache + preloading + deferred data + error boundaries
Navigation resultFire-and-forgetAwaitable outcome (succeeded / cancelled / redirected / …)
View transitionsHand-rolled JSOne option, direction-aware defaults, plain-CSS overrides
Keep-alive page state-Per-route or per-parameter retention with LRU eviction
Typed URLsStringsSource-generated BrouterRoutes builders
Scroll & focus managementFocusOnNavigateScroll-to-top, fragment scrolling, position restoration, focus-on-navigate
@page components & [Authorize]✓ unchanged - discovery, the Found template and the framework's own AuthorizeRouteView

Where to start

Choose your path

New to Brouter?

Install, register, declare two routes - running in five minutes.

Getting started →

On the built-in Router?

Your Found template ports verbatim; @page pages just keep working.

Migration guide →

Want to see it live?

Every feature on this site has a clickable demo - this site is the router.

Open the playground →

Building with an AI agent?

This site is an MCP server: point your agent at /mcp and it reads the shipped library instead of guessing.

Try the MCP server →

Building something specific?

Protected areas, unsaved-changes prompts, query-state filters, breadcrumbs - answered as code.

Browse the recipes →

Looking up a parameter?

Every component parameter, service member, option and value type, with its default.

API reference →

Questions

Frequently asked

Does Brouter replace the built-in Blazor Router?

Yes - it is a drop-in replacement. Point AppAssembly at your app and every @page component is discovered and matched with identical template semantics; the built-in Router's <Found> / <NotFound> / <Navigating> templates port over as-is. See the migration guide.

Does it work with Server, WebAssembly and Auto render modes - and prerendering?

Yes. This site runs the same shared pages under all three hosts. Loader results captured during prerender can be persisted so they do not double-fetch, and on .NET 10 an unmatched URL during static SSR produces a real HTTP 404.

What about [Authorize] pages?

Set NotAuthorized / Authorizing on <Brouter> and Brouter composes the framework's own AuthorizeRouteView internally - authorization correctness stays Microsoft's code, and native rendering fails closed on [Authorize] components it cannot enforce.

Is there any JavaScript to set up?

No. Brouter brings its script along as a static web asset - there is no script tag to add and nothing to bundle. On browsers without the View Transitions API the animations are simply inert.

What does it cost at scale?

Every route is a live component instance: roughly 3-6 KB of retained memory each, so ~500 routes add about 2.5 MB and ~4 ms of startup. A first-segment index keeps per-navigation matching fast regardless. Details and a runnable benchmark are on the performance page.

More answers - including what to check when something does not behave as expected - on the FAQ & troubleshooting page.

Route your Blazor app the modern way

One package, one component, and every @page you already have.

dotnet add package Bit.Brouter