File-based routing allows you to define pages by placing components in a directory structure, eliminating the need for explicit defineModule() and defineResource() calls. This approach is similar to Next.js’s App Router.
Overview
Instead of manually assembling module/resource hierarchies, you define pages as files in a pages/ directory. The route path is automatically derived from the directory structure.
src/pages/
├── page.tsx # / (root path)
├── purchasing/
│ ├── page.tsx # /purchasing
│ └── orders/
│ ├── page.tsx # /purchasing/orders
│ └── [id]/
│ └── page.tsx # /purchasing/orders/:id
└── (admin)/ # Grouping (not included in path)
└── settings/
└── page.tsx # /settings
Setup
Configure Vite plugin
Add the appShellRoutes plugin to your Vite config: // vite.config.ts
import { defineConfig } from 'vite' ;
import react from '@vitejs/plugin-react' ;
import { appShellRoutes } from '@tailor-platform/app-shell-vite-plugin' ;
export default defineConfig ({
plugins: [
react (),
appShellRoutes (), // Scans src/pages by default
] ,
}) ;
Create pages directory
Create a src/pages/ directory in your project.
Use AppShell without modules prop
No configuration needed—pages are automatically discovered: // App.tsx
import { AppShell , SidebarLayout , DefaultSidebar } from '@tailor-platform/app-shell' ;
const App = () => {
return (
< AppShell title = "My App" >
< SidebarLayout sidebar = { < DefaultSidebar /> } />
</ AppShell >
);
};
Pages are automatically discovered and injected—no modules prop required!
Page components
Minimal example
The simplest page is just a default-exported component:
// src/pages/about/page.tsx
export default () => < div > About </ div > ;
Full example
Use the appShellPageProps static field to configure metadata and guards:
// src/pages/dashboard/page.tsx
import type { AppShellPageProps } from '@tailor-platform/app-shell' ;
import { authGuard } from '../guards' ;
import { DashboardIcon } from '../icons' ;
const DashboardPage = () => {
return < div > Dashboard Content </ div > ;
};
DashboardPage . appShellPageProps = {
meta: {
title: "Dashboard" ,
icon: < DashboardIcon /> ,
},
guards: [ authGuard ],
} satisfies AppShellPageProps ;
export default DashboardPage ;
AppShellPageProps type
type AppShellPageProps = {
meta ?: {
title : LocalizedString ;
icon ?: ReactNode
};
guards ?: Guard [];
};
Path conventions
Directory Name Converts To Description ordersordersStatic segment [id]:idDynamic parameter (group)(excluded) Grouping only (not in path) _lib(ignored) Not routed (for shared logic)
Examples
src/pages/
├── users/
│ ├── page.tsx # /users
│ └── [userId]/
│ └── page.tsx # /users/:userId
├── (marketing)/
│ ├── campaigns/
│ │ └── page.tsx # /campaigns (not /marketing/campaigns)
│ └── analytics/
│ └── page.tsx # /analytics
└── _utils/
└── helpers.ts # Not routed (shared utilities)
Use (parentheses) for route grouping without affecting the URL path. Use _underscore for shared code that shouldn’t be routed.
Dynamic parameters
Use square brackets for dynamic route segments:
// src/pages/orders/[id]/page.tsx
import { useParams } from '@tailor-platform/app-shell' ;
const OrderDetailPage = () => {
const { id } = useParams ();
return < div > Order ID: { id } </ div > ;
};
export default OrderDetailPage ;
Multiple parameters
src/pages/orders/[orderId]/items/[itemId]/page.tsx
Converts to route: /orders/:orderId/items/:itemId
Route guards
Guards are not automatically inherited from parent pages. Each page must explicitly define its own guards:
// src/pages/dashboard/page.tsx
DashboardPage . appShellPageProps = {
guards: [ authGuard ],
} satisfies AppShellPageProps ;
// src/pages/dashboard/admin/page.tsx
// Must include authGuard explicitly—not inherited
AdminPage . appShellPageProps = {
guards: [ authGuard , adminGuard ],
} satisfies AppShellPageProps ;
Reusable guards
To share common guards across pages, compose them from a shared module:
// src/guards.ts
import { type Guard , pass , hidden , redirectTo } from "@tailor-platform/app-shell" ;
export const requireAuth : Guard = ({ context }) => {
if ( ! context . currentUser ) {
return redirectTo ( "/login" );
}
return pass ();
};
export const requireAdmin : Guard = ({ context }) => {
if ( context . currentUser ?. role !== "admin" ) {
return hidden ();
}
return pass ();
};
// Composed guard arrays
export const protectedRoute = [ requireAuth ];
export const adminRoute = [ requireAuth , requireAdmin ];
Use in pages:
// src/pages/dashboard/orders/page.tsx
import { protectedRoute } from '@/guards' ;
OrdersPage . appShellPageProps = {
guards: protectedRoute ,
} satisfies AppShellPageProps ;
Typed routes
Enable generateTypedRoutes to generate type-safe route helpers:
// vite.config.ts
appShellRoutes ({
generateTypedRoutes: true ,
})
This generates src/routes.generated.ts with a paths helper:
import { paths } from './routes.generated' ;
import { Link } from '@tailor-platform/app-shell' ;
// Static routes
< Link to = { paths . for ( "/dashboard" ) } > Dashboard </ Link >
// Dynamic routes - params are type-checked
< Link to = { paths . for ( "/orders/:id" , { id: orderId }) } > Order </ Link >
// TypeScript catches errors:
paths . for ( "/orders/:id" ); // ❌ Error: missing 'id'
paths . for ( "/invalid" ); // ❌ Error: route doesn't exist
Type-safe navigation Learn more about typed routes
Comparison with legacy API
Before: Explicit hierarchy
const orderDetailResource = defineResource ({
path: ":id" ,
component: OrderDetail
});
const ordersResource = defineResource ({
path: "orders" ,
component: OrdersList ,
subResources: [ orderDetailResource ],
});
const purchasingModule = defineModule ({
path: "purchasing" ,
component: PurchasingPage ,
resources: [ ordersResource ],
});
< AppShell modules = { [ purchasingModule ] } />
After: File-based pages
// src/pages/purchasing/orders/[id]/page.tsx
const OrderDetailPage = () => < div > Order Detail </ div > ;
OrderDetailPage . appShellPageProps = {
meta: { title: "Order Detail" },
} satisfies AppShellPageProps ;
export default OrderDetailPage ;
// App.tsx - No configuration needed
< AppShell title = "My App" >
< SidebarLayout sidebar = { < DefaultSidebar /> } />
</ AppShell >
Concept mapping
Legacy API File-Based Notes ModuleFirst-level directory Top-level folder in pages/ ResourceDirectory with page.tsx Nested folders defineModule()Not needed Automatic defineResource()Not needed Automatic path propertyDirectory name Auto-derived component propertypage.tsx default exportFile convention meta propertyPage.appShellPageProps.metaStatic field guards propertyPage.appShellPageProps.guardsStatic field (no inheritance) subResourcesSubdirectories Auto-derived
Compatibility
File-based pages and explicit modules prop are mutually exclusive .
Valid patterns
Pattern 1: File-based (recommended)
Pattern 2: Explicit modules (legacy)
Pattern 3: Plugin enabled + modules prop
// vite.config.ts has appShellRoutes() plugin
< AppShell title = "My App" >
< SidebarLayout sidebar = { < DefaultSidebar /> } />
</ AppShell >
No plugin + no modules = runtime error: “No routes configured”
Migration guide
To migrate from defineModule/defineResource to file-based routing:
Add Vite plugin
// vite.config.ts
import { appShellRoutes } from '@tailor-platform/app-shell-vite-plugin' ;
export default defineConfig ({
plugins: [
react (),
appShellRoutes (),
] ,
}) ;
Create pages directory structure
Map each module to a top-level directory
Map each resource to a subdirectory with page.tsx
Use [param] for dynamic segments
Move component and metadata
// Before: defineResource({ path: "orders", component: Orders, meta: {...} })
// After: src/pages/orders/page.tsx
const OrdersPage = () => < Orders /> ;
OrdersPage . appShellPageProps = { meta: { ... } };
export default OrdersPage ;
Remove modules prop
Once all pages are migrated, remove the modules prop from <AppShell>
Routing and navigation Learn about client-side navigation and typed routes
Modules and resources Understand the legacy explicit configuration approach