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.
Route patterns:
index.tsxor_index.tsx- Index routes (match parent folder's path).- Index routes automatically nest under layouts with matching path segmentsβfor example,
admin/index.tsxnests underadmin/_layout.tsx.
- Index routes automatically nest under layouts with matching path segmentsβfor example,
_layout.tsx- Layout with<Outlet />for child routes- Other
_prefixes (like_auth/) create pathless layout groups $param- Dynamic segments (e.g.,$slugβ:slug)$.tsx- Splat routes (catch-all)(segment)- Optional segments (e.g.,(en)βen?)($param)- Optional dynamic params (e.g.,($lang)β:lang?)robots[.]txt.ts(and similar) - Escape a literal.(or other special characters) inside[...]to generate file-like routes such as/robots.txt
Key insight: Folders are just a convenience for organization. Without a parent file, api/users.ts behaves exactly like api.users.ts - both create the same /api/users route.
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 (including anonymous+.tsxfiles and+/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 using the + colocation prefix over ignoredRouteFiles when possible. Ignored files skip all processing including conflict detection, while colocated files still benefit from validation checks like ensuring proper placement. For example, place tests in +test/ folders rather than using **/*.test.{ts,tsx} in ignoredRouteFiles.
Directory resolution notes
routesDirentries stay relative (no absolute paths), but you can point outside the app folder with parent segments like'../pages'. Paths are resolved from the project root (process.cwd()).- When you mount
/to a folder, import prefixes are anchored to that folderβs parent so generatedfilevalues stay short (e.g.,'/': 'packages/web/routes'keepsroutes/*imports instead of../packages/web/routes/*). - Without a
/mount, the app directory defaults to<cwd>/app; override it by definingglobalThis.__reactRouterAppDirectory(React Routerβs config can set this) to match custom app roots such asapp/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.*
In Remix (and remix-flat-routes), dot-delimited files often created sibling routes. In React Router v7, routes that share a path prefix are nested by default.
Example:
users.$id.tsxusers.$id.edit.tsx
In Remix, these might be siblings.
In React Router v7, users.$id.edit.tsx is a child of users.$id.tsx.
Implication:
If users.$id.tsx does not render an <Outlet />, the edit route will never render.
Solution:
If you want them to be siblings (sharing the same layout or just independent), use folders and index.tsx:
routes/
βββ users/
βββ $id/
β βββ index.tsx β /users/:id (The detail view)
β βββ edit.tsx β /users/:id/edit (The edit view)
Now $id is just a folder (path segment), and index.tsx and edit.tsx are siblings.
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.