ワンクリックで
epic-routing
Guide on routing with React Router and react-router-auto-routes for Epic Stack
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Guide on routing with React Router and react-router-auto-routes for Epic Stack
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
Guide on Prisma, SQLite, and LiteFS for Epic Stack
Guide on deployment with Fly.io, multi-region setup, and CI/CD for Epic Stack
Guide on security practices including CSP, rate limiting, and session security for Epic Stack
SOC 職業分類に基づく
| name | epic-routing |
| description | Guide on routing with React Router and react-router-auto-routes for Epic Stack |
| categories | ["routing","react-router","file-based-routing"] |
Use this skill when you need to:
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:
// ✅ 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 <div>{loaderData.user.name}</div>
}
// ❌ 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:
// ✅ 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
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:
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 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
Use $ to indicate route parameters:
Syntax:
$param.tsx → :param in URL$username.tsx → :username in URLExample route with parameter:
// 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 }
}
_layout.tsxUse _layout.tsx to create shared layouts for child routes.
Example:
// 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 (
<main className="container">
<h1>{loaderData.owner.name}'s Notes</h1>
<Outlet /> {/* Child routes render here */}
</main>
)
}
Child routes ($noteId.tsx, index.tsx, etc.) will render where <Outlet />
is.
Resource routes don't render UI; they only return data or perform actions.
Characteristics:
default componentloader or action or bothExample:
// app/routes/resources/healthcheck.tsx
export async function loader(_args: Route.LoaderArgs) {
// Check application health
try {
await prisma.$queryRaw`SELECT 1` // Check DB connectivity
return new Response('OK')
} catch (error) {
return new Response('ERROR', { status: 500 })
}
}
Loaders - Load data before rendering (GET requests) Actions - Handle data mutations (POST, PUT, DELETE)
Loader pattern:
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 <div>{/* Use loaderData.data */}</div>
}
Action pattern:
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 method="POST">
{/* Form fields */}
</Form>
)
}
Access query parameters using useSearchParams:
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 (
<div>
<input
value={query}
onChange={(e) => setSearchParams({ q: e.target.value })}
/>
{/* Results */}
</div>
)
}
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.
_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)// app/routes/products/_layout.tsx
export async function loader({ request }: Route.LoaderArgs) {
const categories = await prisma.category.findMany()
return { categories }
}
export default function ProductsLayout({ loaderData }: Route.ComponentProps) {
return (
<div>
<nav>
{loaderData.categories.map(cat => (
<Link key={cat.id} to={`/products/${cat.slug}`}>
{cat.name}
</Link>
))}
</nav>
<Outlet />
</div>
)
}
// app/routes/products/index.tsx
export default function ProductsIndex() {
return <div>Products list</div>
}
// app/routes/products/$slug.tsx
export async function loader({ params }: Route.LoaderArgs) {
const product = await prisma.product.findUnique({
where: { slug: params.slug },
})
if (!product) {
throw new Response('Not Found', { status: 404 })
}
return { product }
}
export default function ProductPage({ loaderData }: Route.ComponentProps) {
return (
<div>
<h1>{loaderData.product.name}</h1>
<p>{loaderData.product.description}</p>
</div>
)
}
export function ErrorBoundary() {
return (
<GeneralErrorBoundary
statusHandlers={{
404: ({ params }) => (
<p>Product "{params.slug}" not found</p>
),
}}
/>
)
}
// app/routes/resources/download-report.tsx
export async function loader({ request }: Route.LoaderArgs) {
const userId = await requireUserId(request)
const report = await generateReport(userId)
return new Response(report, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="report.pdf"',
},
})
}
// app/routes/users/$username/posts/$postId/comments/$commentId.tsx
export async function loader({ params }: Route.LoaderArgs) {
// params contains: { username, postId, commentId }
const comment = await prisma.comment.findUnique({
where: { id: params.commentId },
include: {
post: {
include: { author: true },
},
},
})
return { comment }
}
react-router-auto-routes, not the standard convention_layout.tsx when you have
shared UI, but don't create layouts unnecessarily<Outlet /> in layouts: Without <Outlet />, child routes
won't render$param.tsx, not
:param.tsx or [param].tsx_auth/) don't appear in the
URLapp/routes.ts - Auto-routes configurationapp/routes/users/$username/notes/_layout.tsx - Example of nested layoutapp/routes/resources/healthcheck.tsx - Example of resource routeapp/routes/_auth/login.tsx - Example of route in route group