Skip to content

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 RouterJuiceNotes
app/page.tsxapp/routes/home.tsxRoot page uses home.tsx, not index.tsx (avoids barrel export confusion)
app/layout.tsxapp/routes/layout.tsxSame concept, same nesting behavior
app/loading.tsxapp/routes/pending.tsxSuspense fallback for the route segment
app/error.tsxapp/routes/catch.tsxError boundary for the route segment
app/not-found.tsxonNotFound optionSet on createRouter, not a file convention
generateMetadata()export const responseresponse.head for title/meta, response.headers for HTTP headers
next/linkLink from @cmj/juice/clientSame API: href, prefetch, client-side navigation
useRouter()useRouter() from @cmj/juice/clientpush, replace, back — same shape
redirect()redirect() from @cmj/juice/runtimeThrows a Response with 303 status
middleware.ts (root)app/routes/middleware.tsPer-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
useActionStateuseActionStateStandard React 19 — same API
Route groups (group)/Not supportedUse flat file naming instead
Parallel routes @slotNot supportedUse component composition in layouts
Intercepting routes (..)Not supportedHandle 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-face with font-display: swap.
  • ISR (Incremental Static Regeneration) — Juice has export const response = { cache: true } for static caching, but no background revalidation. Use CDN cache rules or stale-while-revalidatein your Cache-Control header.
  • API routes (app/api/) — Juice supports API routes via export function GET/POST in 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.cache with aggressive caching.
  • NextRequest / NextResponse — Juice uses standard Request and Response. 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-app

Step 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. Use cache() 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.tsx not page.tsx. Juice uses home.tsx for the index route in a directory, not page.tsx.
  • No params promise. In Next.js 15+, paramsis a promise. In Juice, params is a plain object passed as a prop.
  • No cookies() or headers() functions. Use request.headers directly — the request object is passed as a prop to every page.
  • No automatic fetch deduplication. Next.js patches fetch() globally. Juice uses explicit cache() for deduplication.
  • Dynamic route syntax. Next.js uses [slug] directories. Juice uses :slug in the URLPattern (file-system routes use [slug].tsx files that compile to /:slug patterns).