Pages Registry
Every module that renders UI must maintain a pages registry -- a Pages/index.ts file that maps route names to React components. This is the bridge between C# endpoints and the React frontend.
The Pattern
Each module exports a pages record from Pages/index.ts:
// modules/Customers/src/SimpleModule.Customers/Pages/index.ts
export const pages: Record<string, unknown> = {
'Customers/Browse': () => import('./Browse'),
'Customers/Manage': () => import('./Manage'),
'Customers/Create': () => import('./Create'),
'Customers/Edit': () => import('./Edit'),
};Each key matches the component name passed to Inertia.Render() on the server side, and each value is a lazy import pointing to the React component.
How It Connects to C# Endpoints
On the backend, view endpoints call Inertia.Render with a component name:
// modules/Customers/src/SimpleModule.Customers/Pages/BrowseEndpoint.cs
public class BrowseEndpoint : IViewEndpoint
{
public void Map(IEndpointRouteBuilder app)
{
app.MapGet(
"/browse",
async (ICustomerContracts customers) =>
Inertia.Render(
"Customers/Browse", // <-- This key must exist in Pages/index.ts
new { customers = await customers.GetAllCustomersAsync() }
)
)
.AllowAnonymous();
}
}The string "Customers/Browse" is the key that resolvePage looks up in the module's pages record. If the key does not exist, resolvePage throws an explicit error and the ClientApp surfaces a toast notification.
Missing entries throw an explicit error
If you add a new IViewEndpoint with Inertia.Render("Customers/Something") but forget to add a matching entry in Pages/index.ts:
- The endpoint compiles and runs fine on the server
- Navigating to that page causes
resolvePageto throw:Error: Page "Customers/Something" not found in module "Customers". Available pages: ... - The ClientApp surfaces this via a toast notification (not a silent 404), and the error is logged to the browser console
Always add the pages registry entry immediately when creating a new view endpoint -- the error is visible, but it still breaks navigation for users.
The Rule
For every IViewEndpoint that calls Inertia.Render("ModuleName/PageName", ...), there must be a matching entry in that module's Pages/index.ts:
'ModuleName/PageName': () => import('./PageName'),Adding a New Page Step-by-Step
Create the C# endpoint implementing
IViewEndpoint:csharppublic class DetailsEndpoint : IViewEndpoint { public void Map(IEndpointRouteBuilder app) { app.MapGet("/{id}", (int id, ICustomerContracts customers) => Inertia.Render("Customers/Details", new { customer = ... })); } }Create the React component in the module's
Pages/directory:tsx// modules/Customers/src/SimpleModule.Customers/Pages/Details.tsx import { PageShell } from '@simplemodule/ui'; import type { Customer } from '../types'; export default function Details({ customer }: { customer: Customer }) { return ( <PageShell title={customer.name}> {/* ... */} </PageShell> ); }Register in
Pages/index.tsimmediately:tsexport const pages: Record<string, unknown> = { 'Customers/Browse': () => import('./Browse'), 'Customers/Manage': () => import('./Manage'), 'Customers/Create': () => import('./Create'), 'Customers/Edit': () => import('./Edit'), 'Customers/Details': () => import('./Details'), };Validate with the validation script:
bashnpm run validate-pages
Validating Page Registrations
The validate-pages script automatically checks that all C# endpoints have corresponding TypeScript entries and vice versa.
What It Does
- Scans all
.csfiles in each module'ssrc/{ModuleName}/directory - Finds all
Inertia.Render("ComponentName/...")calls via regex - Reads the module's
Pages/index.tsfile - Extracts all keys from the
pagesrecord - Compares the two lists and reports mismatches
Running It
npm run validate-pagesOn success:
=== Pages Registry Validation ===
All modules have valid Pages/index.ts registrationsOn failure:
=== Pages Registry Validation ===
Module: Customers
Missing in Pages/index.ts: Customers/Details
Found 1 module(s) with mismatches
Please update the Pages/index.ts files to match C# endpoints.Exit Codes
| Code | Meaning |
|---|---|
0 | All modules have valid registrations |
1 | Mismatches found (missing or extra entries) |
CI Integration
The script exits with code 1 on failure, making it suitable for CI pipelines. Add it as a step in your build workflow to catch missing registrations before they reach production.
Lazy vs Eager Imports
The pages registry supports both lazy and eager component imports:
// Lazy (recommended) -- component is loaded on demand
'Customers/Browse': () => import('./Browse'),
// Eager -- component is bundled into the pages.js file
'Customers/Browse': Browse,Lazy imports are recommended because they allow Vite to code-split within the module bundle. The resolvePage function handles both patterns transparently.
Next Steps
- UI Components -- the shared Radix UI component library
- Styling & Theming -- Tailwind CSS configuration and theming
- Vite Build System -- how module bundles are built