GitHubnpm

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;