A simple, type-safe router for React that works with any framework.
- β¨ Super Simple - A tiny API:
createRouteandcreateRouter(plus optionaldefineRoute/defineRoutessugar) - π Type Safe - TypeScript knows your route params automatically
- π― Flexible - Works with any React framework
- β Validated - Built-in query parameter validation
npm install @btst/yar
# or
pnpm add @btst/yarHere's one complete example showing all features:
import { createRoute, createRouter } from "@btst/yar";
import { z } from "zod"; // or any Standard Schema library
import HomePage from "./pages/home";
import BlogPostPage from "./pages/blog-post";
// 1οΈβ£ Simple route - just a page
const homeRoute = createRoute(
"/",
() => ({
PageComponent: HomePage,
meta: () => [{ name: "title", content: "Home" }],
}),
undefined,
{ isStatic: true } // π·οΈ Optional: tag routes for filtering for SSG environments
);
// 2οΈβ£ Dynamic route - with params, validation, and data loading
const blogRoute = createRoute(
"/blog/:slug", // :slug becomes available as params.slug
({ params, query }) => ({
PageComponent: BlogPostPage,
// π¦ Load data (can use AbortSignal, options, etc.)
loader: async (signal?: AbortSignal) => {
const res = await fetch(`/api/posts/${params.slug}`, { signal });
return res.json();
},
// π Generate meta tags (can use loader data)
meta: (post) => [
{ name: "title", content: post.title },
{ name: "description", content: post.excerpt },
],
// π¨ Extra data for anything (breadcrumbs, layout, etc.)
extra: () => ({
breadcrumbs: ["Home", "Blog", params.slug],
layout: "blog",
}),
}),
{
// β
Validate query parameters
query: z.object({
preview: z.boolean().optional(),
}),
},
{ isStatic: false, requiresAuth: false } // π·οΈ Route tags
);
// 3οΈβ£ Create router
const router = createRouter({
home: homeRoute,
blog: blogRoute,
});
// 4οΈβ£ Use the router in your app
async function handleRequest(url: string) {
const route = router.getRoute(url);
if (!route) {
return <NotFoundPage />;
}
// Everything is typed! TypeScript knows the types of all params
const data = route.loader ? await route.loader() : null;
const metaTags = route.meta ? route.meta(data) : [];
const extras = route.extra ? route.extra() : null;
return (
<route.PageComponent
params={route.params}
data={data}
/>
);
}
// 5οΈβ£ Filter routes without running handlers (great for SSG!)
const staticRoutes = Object.values(router.routes)
.filter(route => route.meta?.isStatic);If you don't need custom closures per route, defineRoute and defineRoutes remove the handler wiring entirely. Components receive the route context (params, query) as props, and loader/meta/extra receive it as their first argument:
import { defineRoute, defineRoutes, createRouter } from "@btst/yar";
// Single route
const postRoute = defineRoute("/blog/:slug", {
page: BlogPostPage, // rendered with { params, query } as props
loading: Spinner,
error: ErrorPage,
loader: (ctx, signal?: AbortSignal) =>
fetch(`/api/posts/${ctx.params.slug}`, { signal }).then((r) => r.json()),
meta: (ctx, post) => [{ name: "title", content: post?.title ?? ctx.params.slug }],
extra: (ctx) => ({ breadcrumbs: ["Home", "Blog", ctx.params.slug] }),
});
// Many routes at once, with optional page overrides
const routes = defineRoutes(
{
home: defineRoute("/", { page: HomePage }, undefined, { isStatic: true }),
post: defineRoute("/blog/:slug", {
page: BlogPostPage,
loader: (ctx) => fetchPost(ctx.params.slug),
meta: (ctx) => [{ name: "title", content: ctx.params.slug }],
}),
},
{ pages: { post: CustomPostPage } } // swap a page component per key
);
const router = createRouter(routes);Overridden pages still receive the route context (params, query) as props. defineRoutes also accepts plain createRoute routes; overriding those swaps the PageComponent as-is.
defineRoute returns a regular route, so it composes freely with createRoute routes in the same createRouter call. Use createRoute when you need full control over the handler closure; use defineRoute/defineRoutes for the common declarative case.
Creates a route. The handler returns:
PageComponent- Your React componentloader()- Load data (can accept any params likeAbortSignal)meta()- Generate SEO tags (can accept any params)extra()- Any extra data you need (can accept any params)
The 4th parameter routeMeta lets you tag routes for filtering (e.g., { isStatic: true }).
Combines your routes. Returns:
routes- All your routesgetRoute(path, query?)- Match a URL and get the route
π― Path Parameters
"/blog/:slug" β params.slug is automatically typedβ Query Validation
query: z.object({ sort: z.string() })π·οΈ Route Tags
{ isStatic: true, requiresAuth: false }
// Filter without running handlers - perfect for finding static routes in SSG environments, or for filtering routes that require authentication.π¨ Flexible Functions
loader: (signal) => fetch(url, { signal })
meta: (data) => [{ name: "title", content: data.title }]
extra: (userId) => ({ breadcrumbs: [...], userId })Perfect for prefetching data and generating meta tags in SSR environments, or for adding extra data to your routes.
Contributions are welcome! Please feel free to submit a Pull Request.
MIT