GitHubnpm

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

FileURL PatternNotes
home.tsx/Index route
about.tsx/aboutStatic route
blog/index.tsx/blogDirectory index
blog/[slug].tsx/blog/:slugDynamic segment
product/[id].tsx/product/:idDynamic segment
api/health.ts/api/healthAPI route (no React)
layout.tsxn/aWraps all children
middleware.tsn/aRuns before children
loading.tsxn/aSuspense fallback
error.tsxn/aError 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/users