By default, Modern.js recommends using Convention Routes as the way to define routes. At the same time, Modern.js also provides a config-based routing capability that can be used together with convention routes or used separately.
Config routes are particularly useful in the following scenarios:
In the src directory or each entry directory, you can define a modern.routes.[tj]s file to configure routes:
import { defineRoutes } from '@modern-js/runtime/config-routes'
export default defineRoutes(({ route, layout, page, $ }, fileRoutes) => {
return [
route("home.tsx", "/"),
]
})defineRoutes accepts a callback function with two parameters:
routeFunctions: An object containing route, layout, page, $ functionsfileRoutes: An array of route configurations generated by convention routesBasic signature of route functions:
modern.routes.[tj]sModern.js provides four main route configuration functions:
The first parameter (path) of all route functions is a relative path, which will be concatenated with the parent path to generate the final route path:
"/" or "" represents the root path of the current level:param syntax to represent dynamic parameters"*" syntax to match all pathsroute, layout, page, $ must point to real files in the current project. Files from node_modules and other repositories in Monorepo are not currently supported.@/pages/..., ~/pages/..., etc.); please use relative paths relative to modern.routes.[tj]s.route FunctionThe route function is a general-purpose route configuration function that automatically determines whether to generate a page route or layout route based on whether there are child routes. It can replace layout, page, $ and other functions.
export default defineRoutes(({ route }, fileRoutes) => {
return [
// When no child routes, automatically generates page route
route("home.tsx", "/"),
route("about.tsx", "about"),
// When has child routes, automatically generates layout route
// dashboard/layout.tsx needs to contain <Outlet> to render child routes
route("dashboard/layout.tsx", "dashboard", [
route("dashboard/overview.tsx", "overview"), // Generated path: /dashboard/overview
route("dashboard/settings.tsx", "settings"), // Generated path: /dashboard/settings
]),
// Dynamic routes
route("products/detail.tsx", "products/:id"),
// Multiple paths pointing to the same component
route("user/profile.tsx", "user"),
route("user/profile.tsx", "profile"),
]
})Use cases:
layout FunctionThe layout function is specifically used to configure layout routes, always generates layout components, and must contain child routes:
export default defineRoutes(({ layout, page }, fileRoutes) => {
return [
// Generate layout route with path "/auth", must contain child routes
layout("auth/layout.tsx", "auth", [
page("auth/login/page.tsx", "login"), // Generated path: /auth/login
page("auth/register/page.tsx", "register"), // Generated path: /auth/register
]),
]
})Use cases:
page FunctionThe page function is specifically used to configure page routes, always generates page components:
export default defineRoutes(({ layout, page }, fileRoutes) => {
return [
layout("dashboard/layout.tsx", "dashboard", [
page("dashboard/overview.tsx", "overview"), // Generated path: /dashboard/overview
page("dashboard/settings.tsx", "settings"), // Generated path: /dashboard/settings
]),
]
})Use cases:
<Outlet> child component rendering$ FunctionThe $ function is specifically used to configure wildcard routes for handling unmatched routes:
export default defineRoutes(({ layout, page, $ }, fileRoutes) => {
return [
layout("blog/layout.tsx", "blog", [
page("blog/page.tsx", ""), // Generated path: /blog
page("blog/[id]/page.tsx", ":id"), // Generated path: /blog/:id
$("blog/$.tsx", "*"), // Wildcard route, matches all unmatched paths under /blog
]),
]
})Use cases:
The $ function has the same functionality as the $.tsx file in convention routes, used to catch unmatched route requests.
export default defineRoutes(({ page }, fileRoutes) => {
return [
// Use page function to explicitly specify page route
page("home.tsx", "/"),
page("about.tsx", "about"),
page("contact.tsx", "contact"),
]
})When no path is specified, routes inherit the parent path:
export default defineRoutes(({ layout, page }, fileRoutes) => {
return [
// Use layout function to explicitly specify layout route
// auth/layout.tsx needs to contain <Outlet> to render child routes
layout("auth/layout.tsx", [
page("login/page.tsx", "login"),
page("register/page.tsx", "register"),
]),
]
})The above configuration will generate:
/login → auth/layout.tsx + login/page.tsx/register → auth/layout.tsx + register/page.tsxexport default defineRoutes(({ page }, fileRoutes) => {
return [
page("user.tsx", "user"),
page("user.tsx", "profile"),
page("user.tsx", "account"),
]
})Config routes support dynamic route parameters:
export default defineRoutes(({ page }, fileRoutes) => {
return [
// Required parameter
page("blog/detail.tsx", "blog/:id"),
// Optional parameter
page("blog/index.tsx", "blog/:slug?"),
]
})Config routes automatically discover component-related files without manual configuration. For any component file specified in modern.routes.[tj]s, Modern.js will automatically find the following related files:
// modern.routes.[tj]s
export default defineRoutes(({ route }, fileRoutes) => {
return [
route("pages/profile.tsx", "profile"),
route("pages/user/detail.tsx", "user/:id"),
]
})Modern.js will automatically find and load:
pages/profile.data.ts → Data loaderpages/profile.config.ts → Route configurationpages/profile.error.tsx → Error boundarypages/profile.loading.tsx → Loading componentFor more detailed information about data fetching, please refer to the Data Fetching documentation. For Loading component usage, please refer to Convention Routes - Loading.
Config routes can be used together with convention routes. You can merge routes by modifying the fileRoutes parameter:
Current route structure can be viewed through the modern routes command
The following examples demonstrate how to merge config routes with convention routes:
// modern.routes.ts
import { defineRoutes } from '@modern-js/runtime/config-routes';
export default defineRoutes(({ page }, fileRoutes) => {
// Scenario 1: Override convention routes
// Remove the original shop route and replace with custom component
const shopPageIndex = fileRoutes[0].children?.findIndex(
route => route.id === 'three_shop/page',
);
fileRoutes[0].children?.splice(shopPageIndex!, 1);
fileRoutes[0].children?.push(page('routes/CustomShop.tsx', 'shop'));
// Scenario 2: Supplement convention routes
// Add config routes without corresponding convention routes
fileRoutes[0].children?.push(page('routes/Settings.tsx', 'settings'));
// Scenario 3: Mixed nested routes
// Add config child routes under convention layout routes
const userRoute = fileRoutes[0].children?.find(
(route: any) => route.path === 'user',
);
if (userRoute?.children) {
userRoute.children.push(page('routes/user/CustomTab.tsx', 'custom-tab'));
}
// Scenario 4: Automatic discovery of related files
// Product.tsx will automatically discover Product.data.ts and Product.error.tsx
fileRoutes[0].children?.push(page('routes/Product.tsx', 'product/:id'));
return fileRoutes;
});Since the final structure after route merging may not be intuitive, Modern.js provides debugging commands to help you view complete route information:
# Generate route structure analysis report
npx modern routesThis command will generate the final route structure in the dist/routes-inspect.json file, helping you understand the complete route information after merging.
{
"routes": [
{
"path": "/",
"component": "@_modern_js_src/routes/page",
"data": "@_modern_js_src/routes/page.data",
"clientData": "@_modern_js_src/routes/page.data.client",
"error": "@_modern_js_src/routes/page.error",
"loading": "@_modern_js_src/routes/page.loading"
},
{
"path": "/dashboard",
"component": "pages/admin",
"config": "pages/admin.config",
"error": "pages/admin.error"
},
{
"path": "/user",
"component": "layouts/user",
"children": [
{
"path": "/user/profile",
"component": "@_modern_js_src/routes/user/profile",
"data": "@_modern_js_src/routes/user/profile.data"
}
]
},
{
"path": "@_modern_js_src/routes/blog/:id",
"component": "blog/detail",
"params": ["id"],
"data": "blog/detail.data",
"loading": "blog/detail.loading"
},
{
"path": "/files/*",
"component": "@_modern_js_src/routes/files/list"
}
]
}In multi-entry projects, the report file will be grouped by entry name, where the key is entryName:
{
"main": {
"routes": [
{
"path": "/",
"component": "@_modern_js_src/routes/page",
"data": "@_modern_js_src/routes/page.data",
"error": "@_modern_js_src/routes/page.error",
"loading": "@_modern_js_src/routes/page.loading"
},
{
"path": "/dashboard",
"component": "@_modern_js_src/routes/dashboard",
"config": "@_modern_js_src/routes/dashboard.config"
}
]
},
"admin": {
"routes": [
{
"path": "/",
"component": "@_modern_js_src/routes/dashboard",
"data": "@_modern_js_src/routes/dashboard.data",
"clientData": "@_modern_js_src/routes/dashboard.data.client",
"config": "@_modern_js_src/routes/dashboard.config"
},
{
"path": "/users",
"component": "@_modern_js_src/routes/users",
"data": "@_modern_js_src/routes/users.data",
"error": "@_modern_js_src/routes/users.error",
"loading": "@_modern_js_src/routes/users.loading"
}
]
}
}In addition to basic route information, the report also displays related files found for each route:
data: Server-side data loading file (.data.ts), used to fetch data on the serverclientData: Client-side data loading file (.data.client.ts), used to refetch data on the clienterror: Error boundary file (.error.tsx), used to handle route rendering errorsloading: Loading state component file (.loading.tsx), used to display data loading stateconfig: Route configuration file (.config.ts), used to configure route metadataThese fields are optional and will only be displayed in the report when corresponding files are found. By viewing these fields, you can quickly understand the complete file structure for each route.