Migrate from Next.js
A concrete mapping from Next.js App Router patterns to Juice equivalents. Not everything has a 1:1 replacement — this page is honest about what does not exist.
Concept Mapping
| Next.js App Router | Juice | Notes |
|---|---|---|
app/page.tsx | app/routes/home.tsx | Root page uses home.tsx, not index.tsx (avoids barrel export confusion) |
app/layout.tsx | app/routes/layout.tsx | Same concept, same nesting behavior |
app/loading.tsx | app/routes/pending.tsx | Suspense fallback for the route segment |
app/error.tsx | app/routes/catch.tsx | Error boundary for the route segment |
app/not-found.tsx | onNotFound option | Set on createRouter, not a file convention |
generateMetadata() | export const response | response.head for title/meta, response.headers for HTTP headers |
next/link | Link from @cmj/juice/client | Same API: href, prefetch, client-side navigation |
useRouter() | useRouter() from @cmj/juice/client | push, replace, back — same shape |
redirect() | redirect() from @cmj/juice/runtime | Throws a Response with 303 status |
middleware.ts (root) | app/routes/middleware.ts | Per-directory middleware, not just root. Onion model (before + after). |
'use server' | 'use server' | Same directive, same behavior |
'use client' | 'use client' | Same directive, same behavior |
useActionState | useActionState | Standard React 19 — same API |
Route groups (group)/ | Not supported | Use flat file naming instead |
Parallel routes @slot | Not supported | Use component composition in layouts |
Intercepting routes (..) | Not supported | Handle with client-side state |
What Does Not Exist
Juice intentionally does not include these Next.js features:
next/image— no built-in image optimization. Use<img>with a CDN (Cloudflare Images, imgix, Cloudinary) or build your own optimization pipeline.next/font— no built-in font optimization. Use standard CSS@font-facewithfont-display: swap.- ISR (Incremental Static Regeneration) — Juice has
export const response = { cache: true }for static caching, but no background revalidation. Use CDN cache rules orstale-while-revalidatein your Cache-Control header. - API routes (
app/api/) — Juice supports API routes viaexport function GET/POSTin route files, but there is no separateapp/api/directory convention. API handlers live alongside page routes. - Static site generation (SSG) — Juice is primarily a server-rendered framework. For static pages, use
response.cachewith aggressive caching. NextRequest/NextResponse— Juice uses standardRequestandResponse. No proprietary wrappers.
Migration Strategy
Migrate incrementally, not all at once. Here is the recommended order:
Step 1: Set up the Juice project
bunx @cmj/juice create my-app
cd my-appStep 2: Move layouts first
Copy your root layout. Replace next/font with CSS @font-face. Replace <html> and <body> tags (Juice layouts work the same way). Remove generateMetadata and replace with export const response.
Step 3: Move pages one at a time
Start with the simplest page. If it is a static page with no data fetching, it will work almost unchanged. For pages with data fetching:
- Async server components — these work identically. Copy them over.
fetch()with revalidation — Juice does not have built-in fetch caching. Usecache()from@cmj/juice/runtimefor request-level deduplication, and CDN caching for revalidation.
Step 4: Convert middleware
Next.js middleware runs at the edge before your app. Juice middleware runs inside your app's request handler with an onion model (before and after the route handler). The key difference: Juice middleware has full access to the request context and can modify the response after the route handler runs.
// Next.js middleware.ts
import { NextResponse } from 'next/server';
export function middleware(request) {
if (!request.cookies.get('session')) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
export const config = { matcher: ['/dashboard/:path*'] };// Juice app/routes/dashboard/middleware.ts
import { redirect } from '@cmj/juice/runtime';
export default async function auth(req: Request, next: () => Promise<Response>) {
const session = req.headers.get('Cookie')?.match(/session=([^;]+)/)?.[1];
if (!session) redirect('/login');
return next();
}Step 5: Convert server actions
Server actions work identically. The 'use server' directive, form binding, and useActionState are all the same. Copy them over.
Step 6: Convert client components
Client components ('use client') work identically. Replace next/link with @cmj/juice/client imports. Replace next/navigation's useRouter with Juice's useRouter.
Common Gotchas
home.tsxnotpage.tsx. Juice useshome.tsxfor the index route in a directory, notpage.tsx.- No
paramspromise. In Next.js 15+,paramsis a promise. In Juice,paramsis a plain object passed as a prop. - No
cookies()orheaders()functions. Userequest.headersdirectly — therequestobject is passed as a prop to every page. - No automatic
fetchdeduplication. Next.js patchesfetch()globally. Juice uses explicitcache()for deduplication. - Dynamic route syntax. Next.js uses
[slug]directories. Juice uses:slugin the URLPattern (file-system routes use[slug].tsxfiles that compile to/:slugpatterns).