Skip to main content
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

1

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

Create pages directory

Create a src/pages/ directory in your project.
3

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 NameConverts ToDescription
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 APIFile-BasedNotes
ModuleFirst-level directoryTop-level folder in pages/
ResourceDirectory with page.tsxNested folders
defineModule()Not neededAutomatic
defineResource()Not neededAutomatic
path propertyDirectory nameAuto-derived
component propertypage.tsx default exportFile convention
meta propertyPage.appShellPageProps.metaStatic field
guards propertyPage.appShellPageProps.guardsStatic field (no inheritance)
subResourcesSubdirectoriesAuto-derived

Compatibility

File-based pages and explicit modules prop are mutually exclusive.

Valid patterns

// 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:
1

Add Vite plugin

// vite.config.ts
import { appShellRoutes } from '@tailor-platform/app-shell-vite-plugin';

export default defineConfig({
  plugins: [
    react(),
    appShellRoutes(),
  ],
});
2

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
3

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;
4

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

Build docs developers (and LLMs) love