Skip to content

Quickstart

Scaffold a Juice app, understand why each file exists, and add your first route.

Create Your App

npx @cmj/juice create my-app

The CLI asks for your deploy target. You can also pass it directly:

npx @cmj/juice create my-app --target bun
npx @cmj/juice create my-app --target cloudflare
npx @cmj/juice create my-app --target node
npx @cmj/juice create my-app --target deno

If you are coming from Next.js or Remix, the target question might seem unusual. Those frameworks assume Node.js and bolt on edge support later. Juice starts from WinterCG (the standard Request/Response API) and adapts to each runtime at the entry point only. The target controls what server.ts looks like. Everything else is identical.

Project Structure

The scaffolder creates these files. Each one exists for a specific reason:

my-app/
  package.json
  tsconfig.json
  vite.config.ts          # Build-time config
  flight-manifest.json    # Auto-generated bridge
  .gitignore
  server.ts               # Runtime entry point
  app/
    routes/
      layout.tsx          # Root layout (html, head, body)
      global.css          # Global styles
      home.tsx            # Index route (/)
      middleware.ts        # Root middleware
      product/[id].tsx    # Dynamic route (/product/:id)
      api/health.ts       # API route (/api/health)
    components/
      counter.tsx         # Client component ('use client')

Why are there two config files?

vite.config.ts is build-time. It tells Vite how to compile your app: which files are routes, how to split client and server code, where to emit chunks. You touch this once and forget it.

server.ts is runtime. It imports the manifest and calls createRouter()to produce a fetch handler. This is where you configure middleware, caching, streaming mode, and CSRF. This file runs every time a request comes in.

In Next.js, these concerns are merged into next.config.js. In Juice, they are separate because the build tool (Vite) and the runtime (your server) are different programs that run at different times.

What is flight-manifest.json?

The manifest is the bridge between the compiler and the runtime. The Vite plugin scans yourapp/routes/ directory, discovers route patterns, client components, server actions, and CSS imports, then writes the result to flight-manifest.json.

Do not edit this file. It is auto-generated on every build. It is committed to git so the runtime can read it in production without running Vite. If your routes seem stale, run juice build to regenerate it.

Why layout.tsx wraps everything

The root layout renders <html>, <head>, and <body>. Every page in your app is rendered inside it. Without a layout, Juice would render your component as a fragment with no document structure, and the browser would receive HTML without a doctype. The layout is not optional for a real app.

Nested layouts (like admin/layout.tsx) wrap only the routes in their directory. They chain from root to leaf. This is the same pattern as Next.js App Router layouts, but discovered from the filesystem rather than configured.

Run the Dev Server

cd my-app
bun install
bun run dev

This starts Vite with the Juice plugin. Hot module replacement works for both server and client components. The dev server prints a route table on startup:

  VITE v6.x.x  ready in 150 ms

  Routes:
    /              home.tsx
    /product/:id   product/[id].tsx
    /api/health    api/health.ts

  GET  /              200  12.3ms
  GET  /product/42    200  8.1ms

Your First Change: Add a Route

The fastest way to understand Juice is to add a route and see it appear. Create app/routes/about.tsx:

// app/routes/about.tsx
import React from 'react';

export default function About() {
  return (
    <div>
      <h1>About Us</h1>
      <p>This page exists because the file exists.</p>
    </div>
  );
}

export const response = {
  head: { title: 'About Us' },
};

Save the file and navigate to /about. The dev server picks it up immediately. No router configuration, no restart. The file path is the route.

Or use the CLI:

juice add route about

This generates the same file with the right boilerplate. Use this for dynamic routes too:

juice add route blog/[slug]

Build for Production

bun run build    # Client + SSR bundle
bun run preview  # Run the production build locally

juice build runs two Vite builds: one for client chunks (the JavaScript your browser loads) and one for the server bundle (the code that renders RSC). The manifest connects them. juice preview runs the production build locally so you can catch issues that only appear in production mode (minified code, different module resolution, no HMR).

When NOT to use the scaffolder

If you are adding Juice to an existing Vite project, skip the scaffolder. Install @cmj/juice, add the Vite plugin, create your server.ts, and move your routes into app/routes/. The scaffolder is for greenfield projects.