ructure
Place a file named ai-guardrails.md at the project root. Structure it logically: routing, components, data flow, mutations, environment, and metadata. Use imperative, unambiguous language. LLMs respond best to explicit prohibitions and clear defaults.
Step 2: Enforce Server Component Defaults
The App Router treats every component as a Server Component unless explicitly marked otherwise. AI models trained on React defaults will generate 'use client' preemptively. The guardrail must invert this assumption.
// β AI default (unconstrained)
'use client';
import { useState } from 'react';
export default function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
// ...
}
// β
Constraint-guided output
export default async function UserProfile({ userId }: { userId: string }) {
const user = await fetchUserFromDB(userId);
return <ProfileCard data={user} />;
}
Rationale: Server Components stream HTML directly, eliminate client-side hydration for static content, and allow direct database/API access. Client directives should only appear when browser APIs, event handlers, or React state are strictly required.
Step 3: Route Data Fetching to the Server Layer
Data fetching must occur in Server Components using native async/await. Client-side libraries (SWR, React Query, Axios) belong only in interactive dashboards where real-time polling is required.
// β
Server Component data flow
export default async function DashboardPage() {
const metrics = await fetchMetrics();
return <MetricsView data={metrics} />;
}
// β
Client Component receives props, never fetches
'use client';
export function MetricsView({ data }: { data: Metric[] }) {
return <Chart values={data} />;
}
Rationale: Server-side fetching reduces round trips, enables streaming Suspense boundaries, and prevents waterfall requests. Passing data as props maintains clear separation between server data resolution and client interactivity.
Step 4: Isolate Mutations via Server Actions
Form submissions and data mutations must use Server Actions. This eliminates the need for traditional API routes for most application logic.
// mutations.server.ts
'use server';
export async function updateSettings(formData: FormData) {
const theme = formData.get('theme') as string;
await db.users.update({ theme });
revalidatePath('/settings');
}
// settings-page.tsx
import { updateSettings } from './mutations.server';
export default function SettingsPage() {
return (
<form action={updateSettings}>
<select name="theme">
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
<button type="submit">Save</button>
</form>
);
}
Rationale: Server Actions provide type-safe mutations, automatic CSRF protection, and seamless form integration. They replace 80% of traditional API routes while keeping mutation logic co-located with the UI that triggers it.
Step 5: Map HTTP Endpoints Correctly
Route handlers remain necessary for webhooks, OAuth callbacks, and third-party integrations. They live in app/api/ and export HTTP method functions.
// app/api/webhooks/stripe/endpoint.server.ts
import { NextResponse } from 'next/server';
import { verifyStripeSignature } from '@/lib/stripe';
export async function POST(req: Request) {
const signature = req.headers.get('stripe-signature');
if (!signature) return NextResponse.json({ error: 'Missing signature' }, { status: 400 });
const payload = await req.text();
const event = verifyStripeSignature(payload, signature);
await processStripeEvent(event);
return NextResponse.json({ received: true });
}
Rationale: Route handlers are for external HTTP contracts. Server Actions are for internal application mutations. Mixing them creates architectural ambiguity and complicates testing.
Pitfall Guide
1. The Hydration Trap
Explanation: AI generates 'use client' on components that only render static markup or receive server-fetched props. This forces React to hydrate unnecessary JavaScript, increasing bundle size and TTI.
Fix: Audit every 'use client' directive. Remove it unless the component uses useState, useEffect, browser APIs (window, localStorage), or event handlers (onClick, onChange).
2. The Silent Cache Assumption
Explanation: AI omits cache configuration in fetch() calls, relying on implicit behavior. Next.js defaults to force-cache, which can serve stale data in dynamic routes.
Fix: Always declare cache strategy explicitly: { cache: 'no-store' } for real-time data, { next: { revalidate: 3600 } } for ISR, or { cache: 'force-cache' } for static content.
3. The API Route Relapse
Explanation: AI generates app/api/ endpoints for simple form submissions or CRUD operations that should use Server Actions. This adds unnecessary network overhead and breaks form integration.
Fix: Reserve app/api/ for external integrations (webhooks, OAuth, third-party callbacks). Use 'use server' functions for all internal mutations.
4. The Environment Scope Breach
Explanation: AI exposes server-only secrets to client bundles by prefixing them with NEXT_PUBLIC_ or accessing them in 'use client' components.
Fix: Server variables: no prefix, never imported in client components. Client variables: NEXT_PUBLIC_ prefix required. Validate with TypeScript strict mode and build-time checks.
5. The Layout Content Leak
Explanation: AI places page-specific UI inside layout.tsx files, causing unnecessary re-renders and breaking streaming boundaries.
Fix: layout.tsx should only contain shared UI (navigation, providers, HTML shell). Page content belongs exclusively in page.tsx.
Explanation: AI uses next/head or document.title manipulation for SEO, which is incompatible with App Router streaming.
Fix: Export metadata objects or generateMetadata functions from page.tsx or layout.tsx. Next.js handles SSR metadata injection automatically.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| User profile page with static data | Server Component + async fetch | Streams HTML, zero client JS | Lowest TTI |
| Interactive dashboard with live charts | Client Component + SWR/React Query | Requires real-time polling & client state | Moderate bundle size |
| Contact form submission | Server Action ('use server') | Type-safe, CSRF-protected, form-native | No extra endpoints |
| Stripe webhook handler | Route Handler (app/api/) | External HTTP contract, signature verification | Standard compute |
| SEO-optimized blog post | Server Component + generateMetadata | SSR metadata, streaming content | Optimal crawlability |
Configuration Template
# AI Generation Guardrails β Next.js App Router
## Routing Architecture
- Project uses Next.js App Router (`app/` directory)
- File-system routing only. No `pages/` directory patterns
- Parallel routes: `@folder` naming convention
- Intercepting routes: `(.)`, `(..)`, `(...)` prefixes
## Component Boundaries
- Default: React Server Components (no `'use client'`)
- `'use client'` permitted ONLY for:
- React state (`useState`, `useReducer`)
- Lifecycle effects (`useEffect`)
- Browser APIs (`window`, `document`, `localStorage`)
- Event handlers requiring client interactivity
- Never add `'use client'` preemptively
## Data Flow
- Fetch data directly in Server Components using `async/await`
- Pass resolved data as props to Client Components
- Do not use `useEffect`, SWR, or React Query for server-fetchable data
- Cache strategy must be explicit in every `fetch()` call
## Mutations
- All form submissions and data mutations use Server Actions
- Define actions with `'use server'` in dedicated `*.server.ts` files
- Do not create API routes for internal mutations
## HTTP Endpoints
- External integrations (webhooks, OAuth, third-party APIs) live in `app/api/`
- Export named functions: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`
- Never use `pages/api/` patterns
## File Conventions
- `loading.tsx`: Suspense fallback during data fetching
- `error.tsx`: Error boundary (must include `'use client'`)
- `not-found.tsx`: Handles `notFound()` calls
- `layout.tsx`: Shared UI only (providers, navigation, HTML shell)
- `page.tsx`: Actual route content
## Environment & Security
- Server-only variables: no prefix, never imported in client components
- Client-accessible variables: `NEXT_PUBLIC_` prefix required
- Validate env var usage with TypeScript strict mode
## Metadata & Optimization
- Export `metadata` object or `generateMetadata` function from pages/layouts
- Use `next/image` with explicit `width` and `height`
- Load fonts via `next/font` in root layout
- Never use `next/head` or `document.title` manipulation
Quick Start Guide
- Initialize the guardrail file: Create
ai-guardrails.md at your project root and paste the configuration template above.
- Configure your IDE/AI agent: Ensure your AI assistant is set to read root-level markdown files. Most modern agents (Cursor, Claude, GitHub Copilot) automatically ingest
*.md files at the workspace root.
- Audit existing components: Run a repository-wide search for
'use client', useEffect data fetching, and pages/api/ references. Refactor to match guardrail constraints.
- Validate cache behavior: Add explicit cache options to all
fetch() calls. Test dynamic routes with { cache: 'no-store' } and static routes with { next: { revalidate: N } }.
- Enforce in CI: Add a custom ESLint rule or script that flags
'use client' in non-interactive components and missing cache declarations. Fail builds on architectural drift.
By treating AI generation as a constrained output rather than an open-ended suggestion engine, teams maintain architectural integrity, reduce hydration overhead, and ship Next.js applications that fully leverage the App Router's streaming capabilities. The guardrail file is not a prompt; it is a contract between your codebase and the AI that assists it.