A type-safe route mapping library for React Router that provides declarative route definitions with compile-time validation. Inspired by @remix-run/fetch-router.
- Type-Safe Navigation: Leverage TypeScript for compile-time route validation and parameter inference
- Declarative Route Maps: Define your entire route structure upfront with type-safe route names
- Composable Architecture: Nest routes, combine layouts, and organize routes hierarchically
- Resource-Based Routing: Built-in helpers for RESTful route patterns
- React Router Integration: Seamlessly generates React Router v7 route configurations
- Type Safety: Catch routing errors at compile time, not runtime
- Simplicity: A declarative API that fits in your head
- Standards-Based: Built on React Router's conventions and best practices
# Install with npm
npm add @withsprinkles/react-router-route-map# Install with yarn
yarn add @withsprinkles/react-router-route-map# Install with pnpm
pnpm add @withsprinkles/react-router-route-map# Install with Deno
deno add npm:@withsprinkles/react-router-route-map# Install with Bun
bun add @withsprinkles/react-router-route-mapThis library requires:
@react-router/devv7.0.0 or higher@remix-run/route-patternv0.15.3 or higher
The main purpose of the route map is to organize your routes by name and provide type-safe navigation throughout your application. The route map structure mirrors your application's routing hierarchy and provides compile-time validation for route parameters and navigation.
Here's a simple application with a home page, an about page, and a blog:
import { createRoutes, index, route } from "@withsprinkles/react-router-route-map";
// Define your route map with named routes
export const routes = createRoutes({
home: index("./home.tsx"),
about: route("/about", "./about.tsx"),
blog: {
index: route("/blog", "./blog/index.tsx"),
show: route("/blog/:slug", "./blog/show.tsx"),
},
});
// Export the React Router config
export default routes[RouteConfig];The route map is an object where keys are route names and values are Route objects. You can inspect the types:
type Routes = typeof routes;
// {
// home: Route<'/'>
// about: Route<'/about'>
// blog: {
// index: Route<'/blog'>
// show: Route<'/blog/:slug'>
// }
// }The route map makes it easy to generate type-safe links and navigation throughout your application using the href() method on routes:
import { Link } from "react-router";
import { routes } from "./routes";
export default function Home() {
return (
<div>
<h1>Welcome</h1>
<nav>
<Link to={routes.about.href()}>About Us</Link>
<Link to={routes.blog.index.href()}>Blog</Link>
</nav>
</div>
);
}Routes with parameters are fully type-checked:
import { Link } from "react-router";
import { routes } from "./routes";
export default function BlogIndex() {
const posts = [
{ slug: "hello-world", title: "Hello World" },
{ slug: "typescript-tips", title: "TypeScript Tips" },
];
return (
<div>
<h1>Blog Posts</h1>
{posts.map(post => (
<Link key={post.slug} to={routes.blog.show.href({ slug: post.slug })}>
{post.title}
</Link>
))}
</div>
);
}Use createRoutes() to convert your route map into a React Router configuration:
import { createRoutes, RouteConfig } from "@withsprinkles/react-router-route-map";
const routes = createRoutes({
// ... your route map
});
// In your app/routes.ts file for React Router v7:
export default routes[RouteConfig];This generates the standard React Router configuration that you would normally write by hand:
import { route, index } from "@react-router/dev/routes";
export default [
index("./home.tsx"),
route("about", "./about.tsx"),
route("blog", "./blog/index.tsx"),
route("blog/:slug", "./blog/show.tsx"),
];Routes can be nested inside layout routes to share UI and logic:
import { createRoutes, index, route, layout } from "@withsprinkles/react-router-route-map";
export const routes = createRoutes({
home: index("./home.tsx"),
// Layout route wraps children with shared UI
auth: layout("./auth/layout.tsx", {
login: route("/login", "./auth/login.tsx"),
register: route("/register", "./auth/register.tsx"),
}),
dashboard: layout("./dashboard/layout.tsx", {
index: index("./dashboard/index.tsx"),
settings: route("/settings", "./dashboard/settings.tsx"),
}),
});The layout component renders child routes through the <Outlet /> component:
// app/auth/layout.tsx
import { Outlet } from "react-router";
export default function AuthLayout() {
return (
<div className="auth-container">
<header>
<h1>My App</h1>
</header>
<main>
<Outlet />
</main>
</div>
);
}Use prefix() to add a path prefix to a group of routes without creating a parent route:
import { createRoutes, index, route, prefix } from "@withsprinkles/react-router-route-map";
export const routes = createRoutes({
home: index("./home.tsx"),
// All these routes will be prefixed with /concerts
concerts: prefix("/concerts", {
index: index("./concerts/index.tsx"),
show: route("/:city", "./concerts/city.tsx"),
trending: route("/trending", "./concerts/trending.tsx"),
}),
});
// Generates URLs:
// routes.concerts.index.href() -> '/concerts'
// routes.concerts.show.href({ city: 'austin' }) -> '/concerts/austin'
// routes.concerts.trending.href() -> '/concerts/trending'Note that prefix() modifies the paths but doesn't introduce a new route into the tree. These two route maps are equivalent:
// Using prefix():
prefix('/parent', {
child1: route('/child1', './child1.tsx'),
child2: route('/child2', './child2.tsx'),
})
// Without prefix:
{
child1: route('/parent/child1', './child1.tsx'),
child2: route('/parent/child2', './child2.tsx'),
}The library provides resources() and resource() helpers for creating RESTful route patterns, similar to Rails' resource routing.
Use resources() to create routes for a collection of resources:
import { createRoutes, resources } from "@withsprinkles/react-router-route-map";
export const routes = createRoutes({
users: resources("/users"),
});
type Routes = typeof routes.users;
// {
// index: Route<'/users'> - Lists all users
// new: Route<'/users/new'> - Form to create a new user
// show: Route<'/users/:id'> - Shows a single user
// edit: Route<'/users/:id/edit'> - Form to edit a user
// }By default, resources() generates four routes. You can customize which routes are generated:
export const routes = createRoutes({
// Only generate index and show routes
users: resources("/users", {
only: ["index", "show"],
}),
// Customize the parameter name (default is 'id')
artists: resources("/artists", {
only: ["index", "show"],
param: "artistId",
}),
// Customize route names
products: resources("/products", {
only: ["index", "show", "edit"],
names: {
index: "list",
show: "view",
edit: "update",
},
}),
});
// routes.users.show.href({ id: '123' }) -> '/users/123'
// routes.artists.show.href({ artistId: 'haim' }) -> '/artists/haim'
// routes.products.list.href() -> '/products'
// routes.products.update.href({ id: '456' }) -> '/products/456/edit'Use resource() to create routes for a singleton resource (not part of a collection):
import { createRoutes, resource } from "@withsprinkles/react-router-route-map";
export const routes = createRoutes({
profile: resource("/profile"),
});
type Routes = typeof routes.profile;
// {
// new: Route<'/profile/new'> - Form to create the profile
// show: Route<'/profile'> - Shows the profile
// edit: Route<'/profile/edit'> - Form to edit the profile
// }Note that resource() doesn't have an index route (since it's not a collection) and routes don't include an :id parameter.
You can customize resource routes similarly to resources:
export const routes = createRoutes({
account: resource("/account", {
only: ["show", "edit"],
names: {
show: "view",
edit: "settings",
},
}),
});
// routes.account.view.href() -> '/account'
// routes.account.settings.href() -> '/account/edit'Resources and regular routes can be nested together:
export const routes = createRoutes({
users: {
...resources("/users", { only: ["index", "show"] }),
profile: resource("/users/:userId/profile", { only: ["show", "edit"] }),
},
});
// routes.users.index.href() -> '/users'
// routes.users.show.href({ id: '123' }) -> '/users/123'
// routes.users.profile.show.href({ userId: '123' }) -> '/users/123/profile'
// routes.users.profile.edit.href({ userId: '123' }) -> '/users/123/profile/edit'Routes can include search parameters in their pattern, providing type-safe query string handling:
import { createRoutes, route, prefix } from "@withsprinkles/react-router-route-map";
export const routes = createRoutes({
search: route("/search?q", "./search.tsx"),
products: prefix("/products?category", {
index: route("/?sort", "./products/index.tsx"),
show: route("/:id?variant", "./products/show.tsx"),
}),
});
// Type-safe search parameter usage:
routes.search.href(null, { q: "laptops" });
// -> '/search?q=laptops'
routes.products.index.href(null, { category: "electronics", sort: "price" });
// -> '/products?category=electronics&sort=price'
routes.products.show.href({ id: "123" }, { category: "electronics", variant: "blue" });
// -> '/products/123?category=electronics&variant=blue'
// TypeScript will error on invalid parameters:
routes.search.href(null, { invalid: "param" });
// ^^^^^^^^ Type error!If a path segment starts with :, it becomes a dynamic segment. The type system automatically extracts these parameters:
import { createRoutes, route } from "@withsprinkles/react-router-route-map";
export const routes = createRoutes({
team: route("/teams/:teamId", "./team.tsx"),
product: route("/c/:categoryId/p/:productId", "./product.tsx"),
});
// Type-safe parameter access:
routes.team.href({ teamId: "123" });
// -> '/teams/123'
routes.product.href({ categoryId: "electronics", productId: "456" });
// -> '/c/electronics/p/456'
// TypeScript will error on missing or incorrect parameters:
routes.team.href({ wrong: "123" });
// ^^^^^ Type error!
routes.product.href({ categoryId: "electronics" });
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Type error: missing productId!You can make a route segment optional by adding a ? to the end:
import { createRoutes, route } from "@withsprinkles/react-router-route-map";
export const routes = createRoutes({
categories: route("/:lang?/categories", "./categories.tsx"),
user: route("/users/:userId/edit?", "./user.tsx"),
});
// Both of these work:
routes.categories.href();
// -> '/categories'
routes.categories.href({ lang: "es" });
// -> '/es/categories'Each route points to a file that exports the route's behavior. See the React Router documentation on route modules for details on loaders, actions, and components.
Example route module:
// app/blog/show.tsx
import type { Route } from "./+types/show";
// Loader runs on the server to fetch data
export async function loader({ params }: Route.LoaderArgs) {
const post = await fetchBlogPost(params.slug);
return { post };
}
// Component renders with the loader data
export default function BlogPost({ loaderData }: Route.ComponentProps) {
return (
<article>
<h1>{loaderData.post.title}</h1>
<div>{loaderData.post.content}</div>
</article>
);
}Converts a route map into a typed route structure with React Router configuration.
const routes = createRoutes({
home: index("./home.tsx"),
about: route("/about", "./about.tsx"),
});
// Access routes
routes.home.href(); // -> '/'
routes.about.href(); // -> '/about'
// Get React Router config
export default routes[RouteConfig];Creates an index route that renders at its parent's path:
index("./home.tsx");
// Creates Route<'/'>Creates a route with a specific path pattern:
route("/about", "./about.tsx");
// Creates Route<'/about'>
route("/blog/:slug", "./blog/show.tsx");
// Creates Route<'/blog/:slug'>Creates a route with child routes:
route("/dashboard", "./dashboard/layout.tsx", {
settings: route("/settings", "./dashboard/settings.tsx"),
profile: route("/profile", "./dashboard/profile.tsx"),
});Creates a layout route that wraps children without adding URL segments:
layout("./auth/layout.tsx", {
login: route("/login", "./auth/login.tsx"),
register: route("/register", "./auth/register.tsx"),
});Adds a path prefix to child routes:
prefix("/admin", {
dashboard: route("/dashboard", "./admin/dashboard.tsx"),
users: route("/users", "./admin/users.tsx"),
});
// Generates:
// - /admin/dashboard
// - /admin/usersCreates resource routes for a collection:
resources('/users', {
only?: ['index', 'new', 'show', 'edit'],
param?: 'id', // default parameter name
names?: {
index?: 'list',
new?: 'create',
show?: 'view',
edit?: 'update',
},
})Generates routes:
index:GET /users- Lists all resourcesnew:GET /users/new- Form to create a resourceshow:GET /users/:id- Shows a single resourceedit:GET /users/:id/edit- Form to edit a resource
Creates resource routes for a singleton:
resource('/profile', {
only?: ['new', 'show', 'edit'],
names?: {
new?: 'create',
show?: 'view',
edit?: 'update',
},
})Generates routes:
new:GET /profile/new- Form to create the resourceshow:GET /profile- Shows the resourceedit:GET /profile/edit- Form to edit the resource
Generates a type-safe URL for the route:
// No parameters
route.href();
// Path parameters only
route.href({ id: "123" });
// Path and search parameters
route.href({ id: "123" }, { sort: "name" });
// Search parameters only (for routes without path params)
route.href(null, { q: "search" });TypeScript will validate that:
- Required path parameters are provided
- Parameter names match the route pattern
- Search parameter names match the pattern (if specified)
// app/routes.ts
import { route, index, layout, prefix } from "@react-router/dev/routes";
export default [
index("./home.tsx"),
route("about", "./about.tsx"),
route("blog", "./blog/index.tsx"),
route("blog/:slug", "./blog/show.tsx"),
];
// app/home.tsx
import { Link } from "react-router";
export default function Home() {
// No type safety - easy to make mistakes
return <Link to="/blog/hello-world">Blog Post</Link>;
}// app/routes.ts
import { createRoutes, index, route, RouteConfig } from "@withsprinkles/react-router-route-map";
export const routes = createRoutes({
home: index("./home.tsx"),
about: route("/about", "./about.tsx"),
blog: {
index: route("/blog", "./blog/index.tsx"),
show: route("/blog/:slug", "./blog/show.tsx"),
},
});
export default routes[RouteConfig];
// app/home.tsx
import { Link } from "react-router";
import { routes } from "./routes";
export default function Home() {
// Fully type-safe - catches errors at compile time
return <Link to={routes.blog.show.href({ slug: "hello-world" })}>Blog Post</Link>;
}- Type Safety: Catch routing errors at compile time instead of runtime
- Refactor with Confidence: Change route patterns in one place, TypeScript ensures all usages are updated
- IDE Support: Autocomplete for route names and parameters
- Self-Documenting: Route map serves as documentation for your app's structure
- Centralized Configuration: All routes defined in one place
- No Magic Strings: Replace hardcoded URLs with typed route references
- React Router - The routing library this builds upon
- @remix-run/fetch-router - The inspiration for this library's route map pattern
- @remix-run/route-pattern - The pattern matching library used internally