Core Concepts
Juice is a React 19 RSC framework built on one idea: a route is a function from Request to Response. Everything else follows from that.
Request In, Response Out
Every Juice app is a single fetch handler. The router matches the URL, loads the server component, renders it to HTML, and returns a Response. No proprietary server object, no middleware framework, no globals.
import { createRouter } from '@cmj/juice/runtime';
import manifest from './flight-manifest.json';
export default {
fetch: createRouter(manifest),
};The Manifest Bridge
The flight manifest is the sole bridge between the Vite compiler and the runtime. The runtime never reads the filesystem. It receives a manifest object that maps route patterns to component modules, client chunks, and server actions.
┌─────────────────────────────────────────────────────┐
│ Vite Build │
│ │
│ app/routes/*.tsx ──► flight-manifest.json │
│ 'use client' ──► client chunks │
│ 'use server' ──► server action map │
└──────────────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ createRouter(manifest) │
│ │
│ URL match ──► import(module) ──► React RSC │
│ RSC render ──► HTML stream ──► Response │
└─────────────────────────────────────────────────────┘WinterCG Only
The runtime uses zero Node.js APIs. It runs on any platform that implements the Web standard Request/Response/URL/URLPattern APIs: Cloudflare Workers, Bun, Deno, and Node.js (with polyfills provided by the adapter).
Server Components by Default
Every .tsx file in app/routes/ is a React Server Component. Server components can be async, fetch data directly, and access the request object. They never ship JavaScript to the client.
To make a component interactive, add 'use client' at the top of the file. Juice automatically code-splits client components into separate chunks.
The response Export
Every route can export a response object to control headers, metadata, and streaming behavior. There are three supported shapes:
1. Static Object
export const response = {
head: {
title: 'My Page',
description: 'Page description for SEO',
},
headers: {
'Cache-Control': 'public, max-age=3600',
},
boundary: 'shell',
};2. Headers as a Function
export const response = {
head: { title: 'Dynamic Headers' },
headers: (req: Request) => ({
'X-Request-Id': req.headers.get('x-request-id') ?? '',
}),
};3. Separate Exports (Legacy)
// These still work but the unified `response` export is preferred
export const headers = { 'Cache-Control': 'no-store' };
export const metadata = { title: 'Page Title' };
export const prerender = true;