Routing
Juice uses file-based routing. Every .tsx or .ts file in app/routes/ becomes a route. Layouts, middleware, and error boundaries are co-located with the routes they wrap.
File Conventions
| File | URL Pattern | Notes |
|---|---|---|
home.tsx | / | Index route |
about.tsx | /about | Static route |
blog/index.tsx | /blog | Directory index |
blog/[slug].tsx | /blog/:slug | Dynamic segment |
product/[id].tsx | /product/:id | Dynamic segment |
api/health.ts | /api/health | API route (no React) |
layout.tsx | n/a | Wraps all children |
middleware.ts | n/a | Runs before children |
loading.tsx | n/a | Suspense fallback |
error.tsx | n/a | Error boundary fallback |
Layouts
Layout files wrap all routes in their directory and subdirectories. They chain from root to leaf. Every layout receives children as a prop.
// app/routes/layout.tsx — Root layout
import React from 'react';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head><meta charSet="utf-8" /></head>
<body>{children}</body>
</html>
);
}
// app/routes/admin/layout.tsx — Nested layout
export default function AdminLayout({ children }: { children: React.ReactNode }) {
return (
<div className="admin-shell">
<nav>Admin Nav</nav>
<main>{children}</main>
</div>
);
}Dynamic Routes and redirect()
// app/routes/product/[id].tsx
import { redirect } from '@cmj/juice/runtime';
import type { PageProps } from '@cmj/juice/runtime';
export default async function Product(props: PageProps['/product/:id']) {
const product = await db.find(props.params.id);
if (!product) redirect('/404');
return <h1>{product.name}</h1>;
}Typed PageProps
Juice generates a juice-env.d.ts file at build time that augments the PageProps interface. This gives you compile-time autocomplete on route params.
import type { PageProps } from '@cmj/juice/runtime';
// TypeScript knows props.params.id is a string
export default function Product(props: PageProps['/product/:id']) {
return <h1>Product {props.params.id}</h1>;
}Type-Safe Link
import { Link } from '@cmj/juice/client';
// Renders a standard <a> tag with SPA navigation
// Prefetches RSC payload on hover by default
<Link href="/product/42">View Product</Link>
<Link href="/about" prefetch="viewport">About</Link>
<Link href="/login" replace>Log In</Link>Route-as-API
Export named HTTP method handlers from any route file. When a request matches the route and the method, Juice calls the handler directly instead of rendering React.
// app/routes/api/users.ts
export function GET(req: Request) {
return Response.json({ users: [] });
}
export async function POST(req: Request) {
const body = await req.json();
return Response.json({ created: true }, { status: 201 });
}Adding Routes with the CLI
juice add route about
juice add route blog/[slug]
juice add route api/usersPrevious
Core ConceptsNext
Data Fetching