Config Routes

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.

When to Use Config Routes

Config routes are particularly useful in the following scenarios:

  • Need flexible route control: When the file structure cannot directly map to the desired URL structure
  • Multiple routes pointing to the same component: Need to point different URL paths to the same page component
  • Conditional routes: Dynamically configure routes based on different conditions
  • Legacy project migration: Maintain the original routing structure when migrating from other routing systems
  • Complex route naming: Need to customize route paths without being limited by file directory structure

Basic Usage

In the src directory or each entry directory, you can define a modern.routes.ts file to configure routes:

import { defineRoutes } from '@modern-js/runtime/config-routes'

export default defineRoutes(({ route, layout, page, $ }, fileRoutes) => {
  return [
    route("home.tsx", "/"),
  ]
})

Function Signature Description

defineRoutes accepts a callback function with two parameters:

  1. routeFunctions: An object containing route, layout, page, $ functions
  2. fileRoutes: An array of route configurations generated by convention routes

Basic signature of route functions:

  • First parameter: File path relative to modern.routes.ts
  • Second parameter: Route path (optional, must be a string)
  • Third parameter: Array of child routes (optional)

Route Functions

Modern.js provides four main route configuration functions:

Path Description

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:

  • Root path: "/" or "" represents the root path of the current level
  • Relative path: Child route paths will be concatenated relative to the parent path
  • Dynamic path: Use :param syntax to represent dynamic parameters
  • Wildcard path: Use "*" syntax to match all paths
Info
  • The first parameter (component file path) of functions like route, layout, page, $ must point to real files in the current project. Files from node_modules and other repositories in Monorepo are not currently supported.
  • Path aliases are not supported (such as @/pages/..., ~/pages/..., etc.); please use relative paths relative to modern.routes.ts.

route Function

The 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:

  • Quick route configuration without explicitly specifying page or layout
  • Simplify the complexity of route configuration

layout Function

The 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:

  • Need to explicitly specify a component as a layout component
  • Provide common layout structure for multiple pages
  • Need to share navigation, sidebar and other UI components across multiple routes

page Function

The 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:

  • Need to explicitly specify a component as a page component
  • Ensure the component does not contain <Outlet> child component rendering
  • Provide clearer semantic expression

$ Function

The $ 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:

  • Custom 404 pages
  • Handle all unmatched requests under specific paths
Tip

The $ function has the same functionality as the $.tsx file in convention routes, used to catch unmatched route requests.

Configuring Routes

Basic Example

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"),
  ]
})

Routes Without Path

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:

  • /loginauth/layout.tsx + login/page.tsx
  • /registerauth/layout.tsx + register/page.tsx

Multiple Paths Pointing to the Same Component

export default defineRoutes(({ page }, fileRoutes) => {
  return [
    page("user.tsx", "user"),
    page("user.tsx", "profile"),
    page("user.tsx", "account"),
  ]
})

Dynamic Routes

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.ts, Modern.js will automatically find the following related files:

// modern.routes.ts
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 loader
  • pages/profile.config.ts → Route configuration
  • pages/profile.error.tsx → Error boundary
  • pages/profile.loading.tsx → Loading component

Discovery Rules

  • File location: Related files must be in the same directory as the component file
  • File naming: Related file names are the same as the component file name (excluding extension)
  • Auto-discovery: Modern.js automatically discovers and loads these files
Tip

For more detailed information about data fetching, please refer to the Data Fetching documentation. For Loading component usage, please refer to Convention Routes - Loading.

Route Merging

Config routes can be used together with convention routes. You can merge routes by modifying the fileRoutes parameter:

  1. Override routes: You can actively remove convention routes and replace them with config routes
  2. Supplement routes: You can add new config routes based on convention routes
  3. Mixed usage: You can add config child routes under convention layout routes
Info

Current route structure can be viewed through the modern routes command

Merging Examples

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;
});

Debugging Routes

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 routes

This command will generate the final route structure in the dist/routes-inspect.json file, helping you understand the complete route information after merging.

Report File Examples

Single Entry Scenario

dist/routes-inspect.json
{
  "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"
    }
  ]
}

Multi-entry Scenario

In multi-entry projects, the report file will be grouped by entry name, where the key is entryName:

dist/routes-inspect.json
{
  "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 server
  • clientData: Client-side data loading file (.data.client.ts), used to refetch data on the client
  • error: Error boundary file (.error.tsx), used to handle route rendering errors
  • loading: Loading state component file (.loading.tsx), used to display data loading state
  • config: Route configuration file (.config.ts), used to configure route metadata

These 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.