Routing Philosophy Following Epic Web principles:
Do as little as possible - Keep your route structure simple. Don't create complex nested routes unless you actually need them. Start simple and add complexity only when there's a clear benefit.
Avoid over-engineering - Don't create abstractions or complex route structures "just in case". Use the simplest structure that works for your current needs.
Example - Simple route structure:
```typescript
// β
Good - Simple, straightforward route
// app/routes/users/$username.tsx
export async function loader({ params }: Route.LoaderArgs) {
const user = await prisma.user.findUnique({
where: { username: params.username },
select: { id: true, username: true, name: true },
})
return { user }
}
export default function UserRoute({ loaderData }: Route.ComponentProps) {
return {loaderData.user.name}
}
// β Avoid - Over-engineered route structure
// app/routes/users/$username/_layout.tsx
// app/routes/users/$username/index.tsx
// app/routes/users/$username/_components/UserHeader.tsx
// app/routes/users/$username/_components/UserDetails.tsx
// Unnecessary complexity for a simple user page
```
Example - Add complexity only when needed:
```typescript
// β
Good - Add nested routes only when you actually need them
// If you have user notes, then nested routes make sense:
// app/routes/users/$username/notes/_layout.tsx
// app/routes/users/$username/notes/index.tsx
// app/routes/users/$username/notes/$noteId.tsx
// β Avoid - Creating nested routes "just in case"
// Don't create complex structures before you need them
```
File-based routing with react-router-auto-routes Epic Stack uses react-router-auto-routes instead of React Router's standard convention. This enables better organization and code co-location.
Basic structure:
```
app/routes/
βββ _layout.tsx # Layout for child routes
βββ index.tsx # Root route (/)
βββ about.tsx # Route /about
βββ users/
βββ _layout.tsx # Layout for user routes
βββ index.tsx # Route /users
βββ $username/
βββ index.tsx # Route /users/:username
```
Configuration in app/routes.ts:
```typescript
import { type RouteConfig } from '@react-router/dev/routes'
import { autoRoutes } from 'react-router-auto-routes'
export default autoRoutes({
ignoredRouteFiles: [
'.*',
'*/ .css',
'*/ .test.{js,jsx,ts,tsx}',
'*/__ .*',
'*/ .server.*', // Co-located server utilities
'*/ .client.*', // Co-located client utilities
],
}) satisfies RouteConfig
```
Route Groups Route groups are folders that start with _ and don't affect the URL but help organize related code.
Common examples:
_auth/ - Authentication routes (login, signup, etc.)_marketing/ - Marketing pages (home, about, etc.)_seo/ - SEO routes (sitemap, robots.txt)Example:
```
app/routes/
βββ _auth/
β βββ login.tsx # URL: /login
β βββ signup.tsx # URL: /signup
β βββ forgot-password.tsx # URL: /forgot-password
βββ _marketing/
βββ index.tsx # URL: /
βββ about.tsx # URL: /about
```
Route Parameters Use $ to indicate route parameters:
Syntax:
$param.tsx β :param in URL$username.tsx β :username in URLExample route with parameter:
```typescript
// app/routes/users/$username/index.tsx
export async function loader({ params }: Route.LoaderArgs) {
const username = params.username // Type-safe!
const user = await prisma.user.findUnique({
where: { username },
})
return { user }
}
```
Nested Layouts with `_layout.tsx` Use _layout.tsx to create shared layouts for child routes.
Example:
```typescript
// app/routes/users/$username/notes/_layout.tsx
export async function loader({ params }: Route.LoaderArgs) {
const owner = await prisma.user.findFirst({
where: { username: params.username },
})
return { owner }
}
export default function NotesLayout({ loaderData }: Route.ComponentProps) {
return (
{loaderData.owner.name}'s Notes
{/ Child routes render here /}
)
}
```
Child routes ($noteId.tsx, index.tsx, etc.) will render where is.
Resource Routes (Routes without UI) Resource routes don't render UI; they only return data or perform actions.
Characteristics:
Don't export a default component Export loader or action or both Useful for APIs, downloads, webhooks, etc. Example:
```typescript
// app/routes/resources/healthcheck.tsx
export async function loader({ request }: Route.LoaderArgs) {
// Check application health
const host = request.headers.get('X-Forwarded-Host') ?? request.headers.get('host')
try {
await Promise.all([
prisma.user.count(), // Check DB
fetch(${new URL(request.url).protocol}${host}, {
method: 'HEAD',
headers: { 'X-Healthcheck': 'true' },
}),
])
return new Response('OK')
} catch (error) {
return new Response('ERROR', { status: 500 })
}
}
```
Loaders and Actions Loaders - Load data before rendering (GET requests)
Actions - Handle data mutations (POST, PUT, DELETE)
Loader pattern:
```typescript
export async function loader({ request, params }: Route.LoaderArgs) {
const userId = await requireUserId(request)
const data = await prisma.something.findMany({
where: { userId },
})
return { data }
}
export default function RouteComponent({ loaderData }: Route.ComponentProps) {
return {/ Use loaderData.data /}
}
```
Action pattern:
```typescript
export async function action({ request }: Route.ActionArgs) {
const userId = await requireUserId(request)
const formData = await request.formData()
// Validate and process data
await prisma.something.create({
data: { / ... / },
})
return redirect('/success')
}
export default function RouteComponent() {
return (
{/ Form fields /}
)
}
```
Search Params Access query parameters using useSearchParams:
```typescript
import { useSearchParams } from 'react-router'
export default function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams()
const query = searchParams.get('q') || ''
const page = Number(searchParams.get('page') || '1')
return (
value={query}
onChange={(e) => setSearchParams({ q: e.target.value })}
/>
{/ Results /}
)
}
```
Code Co-location Epic Stack encourages placing related code close to where it's used.
Typical structure:
```
app/routes/users/$username/notes/
βββ _layout.tsx # Layout with loader
βββ index.tsx # Notes list
βββ $noteId.tsx # Note view
βββ $noteId_.edit.tsx # Edit note
βββ +shared/ # Code shared between routes
β βββ note-editor.tsx # Shared editor
βββ $noteId.server.ts # Server-side utilities
```
The + prefix indicates co-located modules that are not routes.
Naming Conventions _layout.tsx - Layout for child routesindex.tsx - Root route of the segment$param.tsx - Route parameter$param_.action.tsx - Route with parameter + action (using _)[.]ext.tsx - Resource route (e.g., robots[.]txt.ts)