@mmth/routes
v0.3.0
Published
A dynamic routing engine for React Router.
Downloads
38
Readme
Routes
A dynamic routing engine for React Router.
Huge thanks to both the Remix team and the Tanstack team for inspiration on this method and pioneering the file and virtual based routing that makes this workflow possible.
Philosophy: Flat files, nested folders, and virtual routes are all the same. You reach for whichever one fits the current complexity or team, and going from one to the other is just a rename and a move. No structure is imposed; the point is to support quick prototyping evolving into a big codebase.
Usage
Integration is simple:
// app/routes.ts
import { routes } from "@mmth/routes";
export default routes();That's it! File based routes work immediately. Want to customise?
// With manual routes and options specified.
import type { RouteConfig } from "@react-router/dev/routes";
import { routes, route, index } from "@mmth/routes";
export default routes(
[index("home.tsx"), route("/special", "./special-handler.tsx")],
{
directory: "",
extensions: ["tsx", "ts", "mdx", "md"],
},
) satisfies RouteConfig;Naming conventions
Everything is a segment. Flat files and folders resolve to the same internal primitive, and all segments resolve to a RouteConfig-compatible node.
| Source | Result |
| ---------------------- | -------------------------------------------------------------------------------------------------------------- |
| index.tsx | Index route for the parent path |
| page.tsx | /page |
| page.about.tsx | /page/about — dots are segment separators |
| folder/page.tsx | /folder/page — folders and dots are interchangeable |
| $param.tsx | Dynamic param → :param |
| $.tsx | Catch-all / splat → * |
| (name).tsx | Optional segment → name? |
| ($param).tsx | Optional param → :param? |
| [sitemap.xml].tsx | Brackets escape special characters → literal sitemap.xml |
| layout.tsx | Wraps every route in its folder (no path segment) |
| (group)/ | Pathless grouping folder; no URL contribution |
| _name.tsx | Named layout module (wraps nothing by itself) |
| _name.page.tsx | page wrapped by the _name layout |
| _name/ | All routes inside wrapped by the sibling _name.tsx layout |
| name_.tsx | Appending an underscore to a route excludes it from the enclosing layout.tsx and from a parent route module" |
| (group)_/ | Same as above but with a folder, excluding the folder and it's children |
| routes.ts (in a dir) | Sub-routes file: the folder owns itself (see below) |
Dots in filenames are segment separators. So api.auth.$.tsx, api/auth.$.tsx, and api/auth/$.tsx all produce the same route. These are interchangable and all valid.
Inspired by this excelent talk by Brooks, a routes.ts in a subdirectory causes that directory to be do it's own route declaration. Sometimes you want certain routes to have their own conventions. The default export gets automatically merged into the top level, and so the subfolder can use pure manual routes, a different routing engine, or whatever they prefer.
Example of natural evolution
# Start simple
routes/
api.auth.$.tsx
# Grow naturally as needed
routes/
api/
auth.$.tsx
assets/
index.tsx
upload.tsxLayouts
Two distinct layout mechanisms are available. As with everything these are interchangable and can coexist in the same directory.
layout.tsx- wrapping layout**
Placing a layout.tsx inside any folder (or (group) folder) wraps every route in that folder. You might recognise this as Next.js' layout convention.
_named- named layout A named layout only wraps routes that share it's name. This is how flat routes in RR/Remix have worked traditionally._dashboard.tsxwraps all sibling routes prefixed with_dashboard:
routes/
_dashboard.tsx
_dashboard.home.tsx
_dashboard.projects.tsx
about.tsxAnd organised in folders:
routes/
_dashboard.tsx
_dashboard/
home.tsx
settings.tsx
about.tsxAnd as always, you can combine all methods no problem:
routes/
_dashboard.tsx
_dashboard/
home.tsx
settings.tsx
_blog.tsx
_blog.blog/
index.tsx
$postId.tsx
(marketing)/
layout.tsxConfiguration
The routes helper function does all the heavy lifting for you, and while it comes with sensible defaults you can configure them all.
Here are the options with their default values:
export default routes([], {
directory: "routes", // the directory the scanner looks for routes in.
extensions: ["tsx", "ts", "jsx", "js"], // the file extensions of routes
prefix: "", // the prefix to wrap all scanned routes in, mainly useful for subfolders.
scan: true // whether to use file based routes at all
});Setting scan: false in a sub-routes file turns off file-based routing for
that folder and everything below it. The parent still excludes the folder,
and any manual routes declared there are still merged in.
Other helper functions
The package comes with it's own index, route, prefix, and layout helper functions. They serve the exact same purpose as React Router's, the only difference is that they're by default relative to the folder set in the routes() options.
