What it gives you
defineRoutes() takes a single route definitions object and returns three things:
routes— the routes object for<Router>nav— navigation helpers (nav.X.push(params),nav.X.replace(params),nav.X.link(params)) with IDE autocomplete on route names and parameter namespaths— URL builders (paths.X(params)) forhrefattributes
Parameter names are extracted from the path pattern at the type level — '/user/:id' infers { id: string | number }, and paths.user({ di: 1 }) (typo) fails at compile time. Calls registerRoutes() for you, so named navigation
via push('routeName') still works.
/define-routes-demo → Interactive playground showing path builders, nav helpers, and the link action format.Quick start
Then in your app shell:
Compile-time safety
The big payoff is in TypeScript / TSX-strict JS. Mistyped route names and mistyped parameter
names both become errors at the call site, not runtime undefineds.
Inferred parameter types come from the :slug tokens in the path pattern. '/orders/:orderId/items/:itemId' infers { orderId: string | number; itemId: string | number }.
Navigation helpers (nav)
Each entry in nav exposes push, replace, and link — the same operations you'd call manually on push/replace from the main module, but with autocomplete bound to that specific route.
Using link() in templates
Or, more concisely, just pass the params shape directly via the array shorthand the link action already supports — defineRoutes() doesn't replace
that path, it just gives you a type-checked alternative.
Path builders (paths)
paths.X(params, query?) returns the URL string for that route. Useful for href attributes (the link action will pick up the path),
analytics, deep-link logging, or anywhere you need the URL without navigating.
Smart optimization
defineRoutes() chooses the right runtime form per route:
- Sync component, no options (
{ path, component }only) — used directly, skippingwrap()overhead. - Async component (
component: () => import(...)) or any option set (conditions, breadcrumbs, permissions, etc.) — automatically routed throughcreateRoute()with the right configuration.
You don't need to think about it — pass the same definition shape regardless of complexity.
All supported route options
Every option you'd normally pass to wrap() or createRoute():
When to use it
✅ Use defineRoutes() when
- You're starting a new app — the cleanest default for v5.2+
- You want compile-time errors on route name / param typos
- You're navigating programmatically in many places — autocomplete pays off
- You'd otherwise be juggling separate
const routes = …andregisterRoutes({ … })calls
⏸ Stick with plain routes + registerRoutes() when
- You're maintaining an existing app and the type safety isn't worth a rewrite
- Routes are mostly flat and you rarely call
push(name)programmatically - You're already happy with
createHierarchy()for tree-structured routes (the two compose —createHierarchy()output can sit alongsidedefineRoutes()output in the same routes prop)
See also
- Named routes — the lower-level
registerRoutes()API thatdefineRoutes()calls internally push/replace/goBack— whatnav.X.push()wraps- Route inheritance — combine with
defineRoutes()for the best of both wrap()— manual route configuration when you don't want the auto-detection