Quickstart
Scaffold a Juice app, understand why each file exists, and add your first route.
Create Your App
npx @cmj/juice create my-appThe 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 denoIf 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 devThis 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.1msYour 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 aboutThis 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 locallyjuice 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.