Automatic folder-based routing with colocation for React Router v7+.
Built on convention-over-configuration principlesβyour file structure defines your routes automatically, with smart defaults that just work, and scale well.
Colocation is a first-class feature:
"Place code as close to where it's relevant as possible" β Kent C. Dodds
Keep your components, tests, utilities, and routes together. No more hunting across folders or artificial separation of concerns. The + prefix marks non-route files for cohesive, feature-based code organization.
- π Flexible file organization - Mix and match folder-based and dot-delimited notation
- π― Prefix-based colocation - Keep helpers and components alongside routes using
+prefix - π¦ Monorepo / sub-apps support - Mount routes from different folders to organize multi-app projects
- β‘ ESM-only - No CommonJS, built for modern tooling
- π§Ή Clean API - Simplified options and intuitive conventions
Install:
npm install -D react-router-auto-routesThe migration CLI relies on your project's own TypeScript install. Make sure
typescript@>=5.0is already indevDependenciesif you plan to runnpx migrate-auto-routes.
Use in your app:
// app/routes.ts
import { autoRoutes } from 'react-router-auto-routes'
export default autoRoutes()Migrating from remix-flat-routes? See the Migration Guide below.
Folder-based structure:
routes/
βββ index.tsx β / (index route)
βββ about.tsx β /about
βββ robots[.]txt.ts β /robots.txt (literal dot segment)
βββ _auth/ β Pathless layout (no /auth in URL)
β βββ _layout.tsx β Auth layout
β βββ login.tsx β /login
β βββ signup.tsx β /signup
βββ blog/
β βββ _layout.tsx β Layout for /blog/* routes
β βββ index.tsx β /blog
β βββ $slug.tsx β /blog/:slug (dynamic param)
β βββ archive.tsx β /blog/archive
βββ dashboard/
β βββ _layout.tsx β Layout for dashboard routes
β βββ index.tsx β /dashboard
β βββ analytics.tsx β /dashboard/analytics
β βββ settings/
β βββ _layout.tsx β Layout for settings routes
β βββ index.tsx β /dashboard/settings
β βββ profile.tsx β /dashboard/settings/profile
βββ files/
βββ $.tsx β /files/* (splat - catch-all)
Equivalent flat (dot-delimited) structure:
routes/
βββ index.tsx β / (index route)
βββ about.tsx β /about
βββ robots[.]txt.ts β /robots.txt (literal dot segment)
βββ _auth._layout.tsx β Auth layout
βββ _auth.login.tsx β /login
βββ _auth.signup.tsx β /signup
βββ blog._layout.tsx β Layout for /blog/* routes
βββ blog.index.tsx β /blog
βββ blog.$slug.tsx β /blog/:slug (dynamic param)
βββ blog.archive.tsx β /blog/archive
βββ dashboard._layout.tsx β Layout for dashboard routes
βββ dashboard.index.tsx β /dashboard
βββ dashboard.analytics.tsx β /dashboard/analytics
βββ dashboard.settings._layout.tsx β Layout for settings routes
βββ dashboard.settings.index.tsx β /dashboard/settings
βββ dashboard.settings.profile.tsx β /dashboard/settings/profile
βββ files.$.tsx β /files/* (splat - catch-all)
Both structures produce identical routes. Use folders for organization, flat files for simplicity, or mix both approaches as needed.
| Pattern | Meaning | Example |
|---|---|---|
index.tsx / _index.tsx |
Index route β the default page for a folder. Automatically nests under a matching _layout.tsx. |
blog/index.tsx β /blog |
_layout.tsx |
Shared layout wrapper (renders an <Outlet />). The only file that creates nesting. layout.tsx without _ is just a normal route (/layout). |
blog/_layout.tsx wraps all /blog/* pages |
_ prefix folder |
Groups routes under a shared layout without adding a URL segment | _auth/login.tsx β /login (not /auth/login) |
$param |
Dynamic segment β matches any value in that position | $slug.tsx β /blog/:slug |
$.tsx |
Catch-all (splat) β matches everything after this point | files/$.tsx β /files/* |
(segment) |
Optional segment β matches with or without it | (en)/about.tsx β /en?/about |
($param) |
Optional dynamic segment | ($lang)/home.tsx β /:lang?/home |
[.] |
Literal dot β escapes the dot so it's part of the URL | robots[.]txt.ts β /robots.txt |
Key insight: Folders are just organization. Without a _layout.tsx in the folder, api/users.ts behaves like api.users.ts β both create a route at /api/users.
When multiple routes could match the same URL, order matters. Routes are sorted by these rules (first match wins):
- Fewer segments first β
/aboutbefore/about/team - Non-index routes before index routes
- Layouts before regular routes
- More specific paths first β
/users/newbefore/users/:idbefore/users/* - Alphabetical by route ID as a final tiebreaker
Keep helpers, components, and utilities alongside routes using the + prefix. Anything starting with + is ignored by the router.
routes/
βββ dashboard/
β βββ index.tsx β Route: /dashboard
β βββ +/
β β βββ helpers.ts
β β βββ types.tsx
β βββ +components/
β βββ data-table.tsx
βββ users/
βββ index.tsx β Route: /users
βββ +user-list.tsx
βββ $id/
βββ index.tsx β Route: /users/:id
βββ edit.tsx β Route: /users/:id/edit
βββ +/
βββ query.ts
βββ validation.ts
Import colocated files using relative paths:
import { formatDate } from './+/helpers'Rules:
- Allowed: Use
+prefixed files and folders anywhere inside route directories (e.g.,+helpers.ts,+.tsx, or+/folders) - Disallowed: Don't place
+entries at the routes root level likeroutes/+helpers.ts(butroutes/_top/+helpers.tsis fine) - Note:
+typesis reserved for React Router's typegen virtual folders so avoid that name.
autoRoutes({
routesDir: 'routes',
ignoredRouteFiles: ['**/.*'], // Ignore dotfiles like .gitkeep
paramChar: '$',
colocationChar: '+',
routeRegex: /\.(ts|tsx|js|jsx|md|mdx)$/,
}).DS_Store is always ignored automatically, even when you provide custom ignoredRouteFiles, and the migration CLI inherits the same default.
Note: Prefer the + colocation prefix over ignoredRouteFiles when possible. Files ignored via ignoredRouteFiles are completely invisible to the router β it won't warn you if they accidentally shadow a route. Colocated + files are still validated, so you'll get helpful errors if something is misplaced. For example, place tests in +test/ folders rather than using **/*.test.{ts,tsx} in ignoredRouteFiles.
Advanced: Directory resolution
- Paths in
routesDirare always relative to your project root. You can use'../pages'to point outside the app folder. - When you mount
/to a folder, generated import paths are kept short relative to that folder's parent (e.g.,'/': 'packages/web/routes'producesroutes/*imports, not../packages/web/routes/*). - Without a
/mount, the app directory defaults to<project>/app. Override this withglobalThis.__reactRouterAppDirectoryif your app lives elsewhere (e.g.,app/router).
routesDir accepts two shapes:
stringβ scan a single root. When omitted, the default'routes'resolves toapp/routesso existing folder structures continue to work with zero config.Record<string, string>β mount filesystem folders to URL paths (key = URL path, value = filesystem folder). Folder paths resolve from the project root so you can mount packages that live outsideapp/.
Mount routes from different folders to organize sub-apps or monorepo packages:
autoRoutes({
routesDir: {
'/': 'app/routes',
'/api': 'api/routes',
'/docs': 'packages/docs/routes',
'/shop': 'packages/shop/routes',
},
})Example structure:
app/
routes/
dashboard.tsx β /dashboard
settings/
_layout.tsx β /settings (layout)
index.tsx β /settings
api/
routes/
users/
index.tsx β /api/users
packages/
docs/
routes/
index.tsx β /docs
shop/
routes/
index.tsx β /shop
Routes from each mount stay isolated when resolving parents and dot-flattening, but still merged into a single manifest.
For a CMS-style setup where you want a homepage (/) and a catch-all for dynamic pages (/*), use a separate index.tsx and $.tsx.
routes/
βββ index.tsx β Homepage (/)
βββ $.tsx β Catch-all (/*)
Why?
Using an optional splat ($).tsx can cause issues with error boundaries bubbling up unexpectedly in React Router v7. Separating them ensures:
- The homepage has its own explicit route and data requirements.
- The catch-all route handles everything else (404s or dynamic CMS pages) without interfering with the root layout's error handling.
Note: This migration tool is designed for projects using remix-flat-routes 0.8.*
This library preserves remix-flat-routes sibling behavior: dot-delimited routes remain siblings by default and only nest under explicit _layout files.
If you want edit to render inside a layout, add a layout file and (optionally) move the detail view to index.tsx:
routes/
βββ users/
βββ $id/
βββ _layout.tsx β Layout for /users/:id/*
βββ index.tsx β /users/:id (The detail view)
βββ edit.tsx β /users/:id/edit (The edit view)
The migration tool continues to follow legacy remix-flat-routes semantics and will promote parent routes to _layout when children exist.
Ensure your project already lists typescript@>=5.0; the CLI resolves the compiler from your workspace.
Install the package, then run the migration CLI:
npx migrate-auto-routes
# or provide an explicit [source] [destination]
npx migrate-auto-routes app/routes app/new-routesThe CLI overwrites the target folder if it already exists. With no arguments it reads from app/routes and writes to app/new-routes. When you pass both arguments, the CLI uses the exact sourceDir and targetDir paths you provide.
Built-in safety checks: The CLI performs these automatically so you donβt have to.
- Verifies you are inside a Git repository and the route source folder (e.g.
app/routes) has no pending changes before running the migration CLI - Runs
npx react-router routesbefore and after rewriting files - Stages the migrated result in
app/new-routes(or your custom target) before swapping it into place - If successful, renames
app/routestoapp/old-routes, then moves the new routes intoapp/routes - If the generated route output differs, prints a diff, restores the original folder, and keeps the migrated files at the target path for inspection
- When your project still imports
createRoutesFromFolders/remix-flat-routes, the CLI updatesapp/routes.tsto exportautoRoutes()so the snapshot check runs against the migrated tree
If everything looks good, you can uninstall the old packages:
npm uninstall remix-flat-routes
npm uninstall @react-router/remix-routes-option-adapterDeprecating legacy route.tsx files in favor of index.tsx plus + colocation. Support remains for now, after which the matcher will be removed.
If you used route.tsx as the entry point for colocated helpers, follow these steps:
- Move any colocated assets (loaders, helpers, tests) into a
+/folder so they stay adjacent without being treated as routes. - Rename each
route.tsxtoindex.tsxinside its directory so the folder name becomes the route segment. - Run
npx react-router routesto confirm the manifest compiles cleanly and no lingeringroute.tsxentries remain. Double-check that colocated helpers stayed inside+/folders so they are not accidentally exposed as routes.
The migration CLI still recognizes route.tsx right now for backwards compatibility, but future releases will warn (and eventually drop support) once projects have had a full cycle to adopt the index.tsx pattern.
- Node.js >= 20
- React Router v7+
This library is heavily inspired by remix-flat-routes by @kiliman. While this is a complete rewrite for React Router v7+, the core routing conventions and ideas stem from that excellent work.