Same React, Different Conventions
Next.js and Remix both build on React, but their conventions diverge significantly. Next.js (App Router): file-based routing in app/ directory, React Server Components as the default, server actions for mutations, streaming SSR with Suspense, and middleware at the edge. Remix: nested route modules with loader/action exports, progressive enhancement with standard HTML forms, full-stack route modules (data + UI in one file), and web standards focus (Request/Response, FormData).
Without framework-specific rules, AI generates: Next.js App Router patterns in a Remix project (server actions do not exist in Remix), Remix loader patterns in Next.js (loaders are a Remix concept, not Next.js), use client directives in Remix (Remix does not have Server Components), or getServerSideProps in a Next.js App Router project (that is Pages Router, not App Router). The meta-framework choice shapes every file in the project โ the AI needs explicit rules.
This article provides: the specific AI rules for each meta-framework, side-by-side comparisons of equivalent patterns, and copy-paste CLAUDE.md templates. The rules prevent the most common meta-framework confusion: mixing App Router and Remix conventions, or mixing App Router with the older Pages Router.
Routing: App Router vs Nested Routes
Next.js App Router: file-based routing in the app/ directory. Each folder is a route segment. page.tsx renders the page, layout.tsx wraps with a persistent layout, loading.tsx provides a Suspense fallback, error.tsx provides an error boundary, and not-found.tsx handles 404s. Dynamic routes: app/blog/[slug]/page.tsx. Route groups: (marketing) folder prefix groups without adding URL segments. AI rule: 'Routes in app/ directory. page.tsx for page content, layout.tsx for persistent layout, loading.tsx for Suspense fallback. Dynamic segments: [param] folder name.'
Remix: route modules in app/routes/ directory. Each file is a route. The file exports: default (the component), loader (server-side data fetching), action (server-side mutation handling), meta (page metadata), and ErrorBoundary (error UI). Nested routes: file naming convention (routes/blog.$slug.tsx or routes/blog_.tsx for layout nesting). Each route module is a full-stack unit: data loading, mutation handling, and UI in one file. AI rule: 'Route modules in app/routes/. Export default (component), loader (GET data), action (POST/PUT/DELETE). File naming for nesting: dot notation for hierarchy.'
The routing rule prevents: AI creating app/routes/ in a Next.js project (wrong directory convention), creating loader exports in Next.js pages (loaders are Remix), using page.tsx in Remix routes (Remix uses the file itself as the route module), or using dot notation for Next.js routes (Next.js uses folder-based nesting). The routing convention is the foundation โ every file location and export depends on it.
- Next.js: app/ directory, page.tsx per route, layout.tsx for wrapping, [param] for dynamic
- Remix: app/routes/ directory, one file per route, loader/action/default exports
- Next.js: folder-based nesting (app/blog/[slug]/page.tsx)
- Remix: file-name-based nesting (routes/blog.$slug.tsx, dot = hierarchy)
- AI error: loader export in Next.js = unused code. page.tsx in Remix = wrong convention
Data Loading: RSC vs Loaders
Next.js data loading: React Server Components fetch data directly with async/await. export default async function BlogPost({ params }: Props) { const post = await db.select().from(posts).where(eq(posts.slug, params.slug)); return <Article post={post} />; }. The component IS the data fetcher. No separate loading function. Streaming: wrap slow data in Suspense for progressive rendering. Client data: TanStack Query or SWR for client-side fetching in Client Components.
Remix data loading: loader functions export server-side data fetching. export async function loader({ params }: LoaderFunctionArgs) { const post = await db.select().from(posts).where(eq(posts.slug, params.slug)); return json({ post }); }. The component uses the data: const { post } = useLoaderData<typeof loader>(). The loader runs on the server; the component renders on the client (or server for SSR). Data and UI are in the same file but separated by export.
The data loading rule is critical: AI generating async component functions in Remix produces errors (Remix components are not async Server Components). AI generating loader exports in Next.js App Router produces dead code (Next.js does not call loader). The paradigm: Next.js = server components ARE the data layer. Remix = loaders are the data layer, components render the data. One rule about data loading prevents the most impactful architectural error.
Next.js: the async Server Component fetches data directly โ no separate loader function. Remix: the loader export fetches data, the component renders it via useLoaderData. Same result, opposite architectures. One rule about data loading prevents the most impactful error.
Mutations: Server Actions vs Remix Actions
Next.js mutations: server actions. Define an async function with 'use server', use it as a form action or call it from a client component. async function createPost(formData: FormData) { 'use server'; await db.insert(posts).values({ title: formData.get('title') }); revalidatePath('/blog'); }. Usage: <form action={createPost}>. Server actions: work without JavaScript (progressive enhancement), support optimistic UI with useOptimistic, and revalidate data with revalidatePath/revalidateTag.
Remix mutations: action functions exported from route modules. export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); await db.insert(posts).values({ title: formData.get('title') }); return redirect('/blog'); }. Usage: <Form method="post">. The <Form> component submits to the route action. Remix actions: work without JavaScript (standard HTML form submission), support optimistic UI with useNavigation, and automatically revalidate all loaders after an action.
The mutation rule: 'Next.js: use server actions (async functions with use server). Form: <form action={serverAction}>. Revalidation: revalidatePath() or revalidateTag().' 'Remix: export action function from the route module. Form: <Form method="post">. Revalidation: automatic after action completes (all loaders re-run).' AI generating 'use server' in Remix: syntax error. AI generating export async function action in Next.js: dead code. The mutation pattern is framework-determined.
- Next.js: server actions with 'use server', <form action={fn}>, revalidatePath/Tag
- Remix: export action from route, <Form method='post'>, auto-revalidate all loaders
- Both: progressive enhancement (forms work without JS) โ different implementation
- Next.js: revalidation is explicit (you call revalidatePath). Remix: automatic (all loaders re-run)
- AI error: 'use server' in Remix = syntax error. export action in Next.js = dead code
Next.js server actions use 'use server' directive. Remix has no such concept โ it exports action functions from route modules. AI generating 'use server' in Remix: build fails. AI generating export action in Next.js: dead code never called. The mutation pattern is framework-determined.
Error Handling: error.tsx vs ErrorBoundary
Next.js error handling: error.tsx files in the App Router. Each route segment can have its own error.tsx that catches errors from the page and its children. error.tsx must be a Client Component ('use client') because it renders in the browser after an error. The error component receives: error (the Error object) and reset (a function to retry rendering). Global fallback: app/global-error.tsx catches errors in the root layout.
Remix error handling: ErrorBoundary exported from route modules. Each route module can export an ErrorBoundary component that catches errors from the loader, action, or component. Remix also supports CatchBoundary for expected errors (HTTP status codes like 404, 403). The ErrorBoundary receives the error via useRouteError(). Remix error boundaries work with nested routes: the nearest ancestor ErrorBoundary catches the error.
The error rule: 'Next.js: create error.tsx (Client Component with use client) in the route directory. Use the reset function for retry. global-error.tsx for root-level errors.' 'Remix: export ErrorBoundary from the route module. Use useRouteError() to access the error. CatchBoundary for expected HTTP errors.' AI generating ErrorBoundary exports in Next.js: ignored (Next.js uses error.tsx files). AI creating error.tsx in Remix: wrong convention (Remix uses exported components).
Ready-to-Use Rule Templates
Next.js App Router CLAUDE.md template: '# Framework (Next.js App Router). Routes in app/ directory: page.tsx (content), layout.tsx (layout), loading.tsx (Suspense), error.tsx (error boundary). Data: async Server Components fetch directly โ no loader functions. Mutations: server actions with use server, <form action={fn}>, revalidatePath. Client interactivity: add use client only for useState/onClick. Styling: Tailwind CSS. Testing: Vitest + React Testing Library. Never: getServerSideProps (Pages Router), loader/action exports (Remix), Vue syntax.'
Remix CLAUDE.md template: '# Framework (Remix). Routes in app/routes/: one file per route, dot notation for nesting. Data: export loader function for GET data, useLoaderData in component. Mutations: export action function, <Form method="post">, redirect() for navigation. Error: export ErrorBoundary, useRouteError() for error access. Progressive enhancement: forms work without JS. Styling: Tailwind CSS. Testing: Vitest + Testing Library. Never: server actions, use server, use client, page.tsx/layout.tsx (Next.js).'
The negative rules are critical for meta-frameworks: Next.js developers must never see loader/action patterns (Remix). Remix developers must never see server actions or use client (Next.js). The negative rule creates a hard boundary that prevents the most confusing cross-contamination โ where the code almost works but uses the wrong framework convention.
Negative rules create hard boundaries: 'Never loader/action in Next.js' and 'Never use server in Remix.' Without them: AI generates code that almost works but uses the wrong convention. The code compiles but the feature is broken. Hard boundary rules prevent the most confusing bugs.
Comparison Summary
Summary of Next.js vs Remix AI rules.
- Routing: Next.js app/ with page.tsx/layout.tsx vs Remix app/routes/ with dot notation nesting
- Data: Next.js async RSC (component IS the fetcher) vs Remix loader export (separate from component)
- Mutations: Next.js server actions (use server) vs Remix action export (<Form method='post'>)
- Errors: Next.js error.tsx files vs Remix ErrorBoundary exports
- Revalidation: Next.js explicit (revalidatePath) vs Remix automatic (all loaders re-run after action)
- Both: progressive enhancement, SSR, React-based โ different conventions for every feature
- Negative rules: 'Never loader/action' in Next.js, 'Never use server/use client' in Remix
- Templates: one paragraph prevents all cross-framework convention errors