Cursor Rules for Next.js: 6 Rules That Make AI Write…
    Neura MarketNeura Market/Cursor
    ChatGPTChatGPTClaudeClaudeGeminiGeminiCursorCursorGrokGrokPerplexityPerplexityDeepSeekDeepSeek
    CoPilotCoPilotStable DiffusionStable DiffusionMidjourneyMidjourney
    View All Directories
    OverviewRulesPromptsMCPsAgentsGamesBlogVideosGuidesCoursesCommunityExtensionsTrending
    CursorBlogCursor Rules for Next.js: 6 Rules That Make AI Write Production-Ready Next.js Code
    Back to Blog
    Cursor Rules for Next.js: 6 Rules That Make AI Write Production-Ready Next.js Code
    nextjs

    Cursor Rules for Next.js: 6 Rules That Make AI Write Production-Ready Next.js Code

    Olivia Craft May 1, 2026
    0 views

    Cursor Rules for Next.js: 6 Rules That Make AI Write Production-Ready Next.js Code Cursor...

    Cursor Rules for Next.js: 6 Rules That Make AI Write Production-Ready Next.js Code

    Cursor can scaffold a Next.js app in minutes. The problem? Without guidance, it generates Pages Router code in App Router projects, slaps "use client" on every file, skips image optimization, leaks environment variables to the browser, and treats req.body like it's already validated.

    You can fix this by adding targeted rules to your .cursorrules or .cursor/rules/*.mdc files. Here are 6 rules I use on every Next.js project, with bad vs. good examples showing exactly what changes.


    Rule 1: Always Use App Router — Never Pages Router

    Always use the Next.js App Router (app/ directory).
    Never create files in pages/. Use file-based routing with
    app/route-name/page.tsx for pages and app/route-name/layout.tsx
    for layouts. Use loading.tsx, error.tsx, and not-found.tsx
    for built-in UI states.
    

    The App Router is the default since Next.js 13.4. Pages Router is legacy. Mixing both creates routing conflicts and confusing behavior.

    Without this rule, Cursor often defaults to Pages Router patterns:

    // ❌ Bad: Pages Router pattern
    // pages/dashboard.tsx
    import { GetServerSideProps } from 'next';
    
    export const getServerSideProps: GetServerSideProps = async (ctx) => {
      const session = await getSession(ctx);
      const stats = await fetchDashboardStats(session.user.id);
    
      return {
        props: { session, stats },
      };
    };
    
    export default function Dashboard({ session, stats }) {
      return (
        <div>
          <h1>Welcome, {session.user.name}</h1>
          <StatsGrid stats={stats} />
        </div>
      );
    }
    

    getServerSideProps, getStaticProps, getInitialProps — all legacy APIs that don't exist in App Router.

    With this rule, Cursor uses App Router conventions:

    // ✅ Good: App Router with server component
    // app/dashboard/page.tsx
    import { getSession } from '@/lib/auth';
    import { fetchDashboardStats } from '@/lib/data';
    import { StatsGrid } from '@/components/StatsGrid';
    
    export default async function DashboardPage() {
      const session = await getSession();
      const stats = await fetchDashboardStats(session.user.id);
    
      return (
        <div>
          <h1>Welcome, {session.user.name}</h1>
          <StatsGrid stats={stats} />
        </div>
      );
    }
    

    No getServerSideProps. No props drilling from data fetching. The component itself is async and fetches data directly on the server.


    Rule 2: Server Components by Default — Client Only When Needed

    All components are React Server Components by default.
    Only add "use client" when the component uses browser APIs,
    React hooks (useState, useEffect, useRef, etc.), or event
    handlers (onClick, onChange, etc.). Never put "use client"
    at the top of page.tsx or layout.tsx unless absolutely required.
    Push "use client" to the smallest leaf component possible.
    

    Server Components render on the server with zero client-side JavaScript. Adding "use client" unnecessarily bloats the bundle and kills performance.

    Without this rule, Cursor puts "use client" everywhere:

    // ❌ Bad: entire page is a client component for no reason
    // app/products/page.tsx
    "use client";
    
    import { useState, useEffect } from 'react';
    
    export default function ProductsPage() {
      const [products, setProducts] = useState([]);
    
      useEffect(() => {
        fetch('/api/products')
          .then(res => res.json())
          .then(setProducts);
      }, []);
    
      return (
        <div>
          <h1>Products</h1>
          {products.map(product => (
            <div key={product.id}>
              <h3>{product.name}</h3>
              <p>${product.price}</p>
              <AddToCartButton productId={product.id} />
            </div>
          ))}
        </div>
      );
    }
    

    The entire page is client-rendered. The product list fetches from an API route instead of querying the database directly. The user downloads all the JavaScript for a page that could have been static HTML.

    With this rule, Cursor keeps the page as a Server Component and only uses "use client" for interactive parts:

    // ✅ Good: Server Component page with a client leaf
    // app/products/page.tsx
    import { db } from '@/lib/db';
    import { AddToCartButton } from '@/components/AddToCartButton';
    
    export default async function ProductsPage() {
      const products = await db.product.findMany({
        orderBy: { createdAt: 'desc' },
      });
    
      return (
        <div>
          <h1>Products</h1>
          {products.map(product => (
            <div key={product.id}>
              <h3>{product.name}</h3>
              <p>${product.price}</p>
              <AddToCartButton productId={product.id} />
            </div>
          ))}
        </div>
      );
    }
    
    // components/AddToCartButton.tsx
    "use client";
    
    import { useState } from 'react';
    
    export function AddToCartButton({ productId }: { productId: string }) {
      const [adding, setAdding] = useState(false);
    
      async function handleClick() {
        setAdding(true);
        await fetch('/api/cart', {
          method: 'POST',
          body: JSON.stringify({ productId }),
        });
        setAdding(false);
      }
    
      return (
        <button onClick={handleClick} disabled={adding}>
          {adding ? 'Adding...' : 'Add to Cart'}
        </button>
      );
    }
    

    The page ships zero JavaScript. Only the button — the one interactive piece — is a client component.


    Rule 3: TypeScript Strict Mode — Always

    Always use TypeScript with strict mode enabled.
    Never use `any` type. Use `unknown` for truly unknown types
    and narrow with type guards. Define explicit return types
    for all API route handlers and server actions. Use Zod schemas
    to derive types from validation where possible.
    

    Without strict mode, TypeScript gives you a false sense of safety. any spreads like a virus through your codebase, silently disabling type checking.

    Without this rule:

    // ❌ Bad: any types everywhere, no strict mode
    export async function POST(req: any) {
      const data = await req.json();
      const user = await createUser(data);
      return Response.json(user);
    }
    
    function formatPrice(price: any) {
      return '$' + price.toFixed(2); // runtime crash if price is a string
    }
    
    async function getUser(id: any) {
      const res = await fetch(`/api/users/${id}`);
      const data: any = await res.json();
      return data;
    }
    

    Everything is any. Nothing is checked. You find bugs in production, not at build time.

    With this rule:

    // ✅ Good: strict types, explicit return types
    import { NextRequest, NextResponse } from 'next/server';
    import { z } from 'zod';
    
    const CreateUserSchema = z.object({
      name: z.string().min(1),
      email: z.string().email(),
      role: z.enum(['admin', 'user']),
    });
    
    type CreateUserInput = z.infer<typeof CreateUserSchema>;
    
    export async function POST(req: NextRequest): Promise<NextResponse> {
      const body: unknown = await req.json();
      const data = CreateUserSchema.parse(body);
      const user = await createUser(data);
      return NextResponse.json(user);
    }
    
    function formatPrice(price: number): string {
      return '$' + price.toFixed(2);
    }
    
    async function getUser(id: string): Promise<User> {
      const res = await fetch(`/api/users/${id}`);
      const data: unknown = await res.json();
      return UserSchema.parse(data);
    }
    

    Every function has explicit input and output types. Zod validates at runtime. TypeScript catches the rest at build time.


    Rule 4: API Routes — Always Validate With Zod, Never Trust req.body

    All API route handlers (app/api/**/route.ts) must validate
    incoming data using Zod schemas. Never access req.body or
    req.json() fields directly without validation. Always return
    proper HTTP status codes. Always handle validation errors
    with a 400 response including the error details.
    

    Your API routes are a public attack surface. Trusting req.body means trusting the internet.

    Without this rule:

    // ❌ Bad: no validation, trusting req.body blindly
    // app/api/orders/route.ts
    export async function POST(req: Request) {
      const { productId, quantity, shippingAddress } = await req.json();
    
      const order = await db.order.create({
        data: {
          productId,
          quantity,
          shippingAddress,
          total: quantity * (await getProductPrice(productId)),
        },
      });
    
      return Response.json(order);
    }
    

    What if quantity is -5? What if productId is an SQL injection string? What if shippingAddress is missing? No validation means anything goes.

    With this rule:

    // ✅ Good: Zod validation, proper error handling
    // app/api/orders/route.ts
    import { NextRequest, NextResponse } from 'next/server';
    import { z } from 'zod';
    
    const CreateOrderSchema = z.object({
      productId: z.string().uuid(),
      quantity: z.number().int().min(1).max(99),
      shippingAddress: z.object({
        street: z.string().min(1),
        city: z.string().min(1),
        state: z.string().length(2),
        zip: z.string().regex(/^\d{5}(-\d{4})?$/),
      }),
    });
    
    export async function POST(req: NextRequest): Promise<NextResponse> {
      const body: unknown = await req.json();
      const result = CreateOrderSchema.safeParse(body);
    
      if (!result.success) {
        return NextResponse.json(
          { error: 'Invalid request', details: result.error.flatten() },
          { status: 400 }
        );
      }
    
      const { productId, quantity, shippingAddress } = result.data;
    
      const order = await db.order.create({
        data: {
          productId,
          quantity,
          shippingAddress,
          total: quantity * (await getProductPrice(productId)),
        },
      });
    
      return NextResponse.json(order, { status: 201 });
    }
    

    Every field is validated. Bad input gets a 400 with details. Quantity can't be negative. Product ID must be a UUID. The zip code must match the expected format.


    Rule 5: Image Optimization — Always next/image, Never img

    Always use the next/image component (import Image from 'next/image')
    for all images. Never use the HTML <img> tag. Always provide width
    and height props, or use fill with a sized parent container.
    Always include a descriptive alt attribute. Use priority prop
    for above-the-fold images (hero, LCP).
    

    <img> tags serve unoptimized images, cause layout shift (CLS), and hurt your Core Web Vitals score. next/image handles lazy loading, resizing, format conversion, and layout stability automatically.

    Without this rule:

    // ❌ Bad: plain img tags, no optimization
    function ProductCard({ product }) {
      return (
        <div>
          <img src={product.imageUrl} />
          <h3>{product.name}</h3>
          <p>${product.price}</p>
        </div>
      );
    }
    
    function HeroSection() {
      return (
        <section>
          <img
            src="/hero-banner.png"
            style={{ width: '100%', height: 'auto' }}
          />
          <h1>Welcome to our store</h1>
        </section>
      );
    }
    

    No alt text (accessibility violation). No width/height (layout shift). Full-size images served to mobile devices. The hero image blocks rendering because it's not prioritized.

    With this rule:

    // ✅ Good: next/image with proper optimization
    import Image from 'next/image';
    
    function ProductCard({ product }: { product: Product }) {
      return (
        <div>
          <Image
            src={product.imageUrl}
            alt={product.name}
            width={400}
            height={300}
            className="rounded-lg object-cover"
          />
          <h3>{product.name}</h3>
          <p>${product.price}</p>
        </div>
      );
    }
    
    function HeroSection() {
      return (
        <section className="relative h-[500px]">
          <Image
            src="/hero-banner.png"
            alt="Store hero banner with featured products"
            fill
            className="object-cover"
            priority
          />
          <h1 className="relative z-10">Welcome to our store</h1>
        </section>
      );
    }
    

    Images are lazy-loaded by default, served in WebP/AVIF, resized per viewport, and include proper alt text. The hero image uses priority for instant LCP.


    Rule 6: Environment Variables — Server-Only Secrets, Never Exposed to Client

    Environment variables without the NEXT_PUBLIC_ prefix are
    server-only and must never be imported in client components.
    API keys, database URLs, and secrets must never use NEXT_PUBLIC_.
    For client-side config, use NEXT_PUBLIC_ prefix and never put
    sensitive values in them. Always access env vars through a
    centralized config file with validation, never inline process.env.
    

    One misplaced NEXT_PUBLIC_ prefix leaks your API key to every browser that loads your app. This is the most dangerous mistake AI can make in a Next.js project.

    Without this rule:

    // ❌ Bad: secrets exposed to the client
    // .env
    NEXT_PUBLIC_DATABASE_URL=postgresql://admin:secret@db.example.com:5432/prod
    NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_abc123
    NEXT_PUBLIC_JWT_SECRET=super-secret-jwt-key
    
    // components/PaymentForm.tsx
    "use client";
    
    export function PaymentForm() {
      async function handleSubmit(data: FormData) {
        // Stripe secret key is in the client bundle!
        const res = await fetch('https://api.stripe.com/v1/charges', {
          headers: {
            Authorization: `Bearer ${process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY}`,
          },
          body: data,
        });
      }
    
      return <form onSubmit={handleSubmit}>...</form>;
    }
    

    Your database password, Stripe secret key, and JWT secret are in the client-side JavaScript bundle. Anyone can open DevTools and read them.

    With this rule:

    // ✅ Good: server-only secrets, validated config
    // .env
    DATABASE_URL=postgresql://admin:secret@db.example.com:5432/prod
    STRIPE_SECRET_KEY=sk_live_abc123
    JWT_SECRET=super-secret-jwt-key
    NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xyz789
    NEXT_PUBLIC_APP_URL=https://myapp.com
    
    // lib/env.ts
    import { z } from 'zod';
    
    const serverSchema = z.object({
      DATABASE_URL: z.string().url(),
      STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
      JWT_SECRET: z.string().min(32),
    });
    
    const clientSchema = z.object({
      NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().startsWith('pk_'),
      NEXT_PUBLIC_APP_URL: z.string().url(),
    });
    
    export const serverEnv = serverSchema.parse(process.env);
    export const clientEnv = clientSchema.parse({
      NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
      NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
    });
    
    // app/api/payment/route.ts — server only
    import { serverEnv } from '@/lib/env';
    
    export async function POST(req: NextRequest): Promise<NextResponse> {
      const stripe = new Stripe(serverEnv.STRIPE_SECRET_KEY);
      // Secret key never leaves the server
    }
    

    Secrets stay on the server. Public values are explicitly prefixed and validated. If an env var is missing or malformed, the app fails at startup — not in production.


    Copy-Paste Ready: All 6 Rules

    Here's a single block you can drop into your .cursorrules or .cursor/rules/nextjs.mdc:

    # Next.js Rules
    
    ## Routing
    - Always use App Router (app/ directory), never Pages Router (pages/)
    - Use file conventions: page.tsx, layout.tsx, loading.tsx, error.tsx, not-found.tsx
    - Never use getServerSideProps, getStaticProps, or getInitialProps
    
    ## Server vs Client Components
    - All components are Server Components by default
    - Only add "use client" for components using hooks, event handlers, or browser APIs
    - Push "use client" to the smallest leaf component possible
    - Never put "use client" on page.tsx or layout.tsx unless absolutely required
    
    ## TypeScript
    - Always use strict mode
    - Never use `any` — use `unknown` and narrow with type guards
    - Define explicit return types for API routes and server actions
    - Use Zod schemas to derive types where possible
    
    ## API Routes
    - Validate all incoming data with Zod schemas
    - Never access req.json() fields without validation
    - Return proper HTTP status codes (400 for bad input, 201 for created)
    - Always use safeParse and return error details on failure
    
    ## Images
    - Always use next/image, never <img>
    - Always provide width + height, or use fill with a sized parent
    - Always include descriptive alt text
    - Use priority for above-the-fold / LCP images
    
    ## Environment Variables
    - Never use NEXT_PUBLIC_ for secrets (API keys, DB URLs, tokens)
    - Access env vars through a centralized validated config, not inline process.env
    - Validate all env vars at startup with Zod
    - Only NEXT_PUBLIC_ vars are safe for client components
    

    Want 50+ Production-Tested Rules?

    These 6 rules are a starting point. My Cursor Rules Pack v2 includes 50+ rules covering React, TypeScript, Next.js, Prisma, and testing — organized by framework and priority so Cursor applies them consistently.

    Stop fighting bad AI output. Give Cursor the rules it needs to write production-ready Next.js code.

    Tags

    nextjscursortypescriptreact

    Comments

    More Blog

    View all
    Cursor Automations in 2026: wire event-triggered coding agents to Slack, CI, and timerscursor

    Cursor Automations in 2026: wire event-triggered coding agents to Slack, CI, and timers

    Cursor Automations in 2026: wire event-triggered coding agents to Slack, CI, and...

    M
    Manu Shukla
    Index Everything, or Read Everything? The Dilemma of Feeding Specs to AI in Multi-Repo Developmentai

    Index Everything, or Read Everything? The Dilemma of Feeding Specs to AI in Multi-Repo Development

    The specs exist. The AI just can't see them. I've always been the type who builds hobby...

    S
    Shunya Shida
    Connect Claude Code, Cursor and Codex to Amazon Bedrock's new console (2026)amazonbedrock

    Connect Claude Code, Cursor and Codex to Amazon Bedrock's new console (2026)

    Connect Claude Code, Cursor and Codex to Amazon Bedrock's new console (2026) Summary. On 5...

    M
    Manu Shukla
    Spotting AI UI is too easyai

    Spotting AI UI is too easy

    There is a weird uncanny valley with LLM-generated UI right now. The code functions perfectly, but if...

    H
    Harish .s
    Seven ranking frameworks, one search page, zero translation tablesai

    Seven ranking frameworks, one search page, zero translation tables

    I went down a rabbit hole this morning reading the late-2025 Juejin AI roundups side by side, and the...

    N
    ninghonggang
    Zendesk MCP: Let Claude Handle Your Support Ticketsmcp

    Zendesk MCP: Let Claude Handle Your Support Tickets

    Install guide and config at curatedmcp.com Zendesk MCP: Let Claude Handle Your Support...

    C
    curatedmcp

    Stay up to date

    Get the latest Cursor prompts, rules, and resources delivered to your inbox weekly.

    Neura Market LogoNeura Market

    Discover the best AI prompts, plugins, and resources for Cursor and more.

    Content Types

    • Rules
    • Prompts
    • MCPs
    • Agents
    • Guides

    Platforms

    • ChatGPT Directory
    • Claude Directory
    • Gemini Directory
    • Cursor Directory
    • Grok Directory
    • Perplexity Directory
    • DeepSeek Directory
    • CoPilot Directory
    • Stable Diffusion Directory
    • Midjourney Directory
    • All Directories

    Resources

    • Blog
    • Documentation
    • Help Center
    • Marketplace

    Legal

    • Privacy Policy
    • Terms of Service

    © 2026 Neura Market. All rights reserved.

    |

    Not affiliated with any AI platform vendors.

    Neura Market

    Custom AI Systems & Services

    Our team of experienced AI builders will help build custom AI systems, workflows, and solutions for your business.

    Request custom work

    Ready-made automations for this

    Workflows from the Neura Market marketplace related to this Cursor resource

    • Make a prefilled Airtable link for a new record in HubSpot CRM and shorten it by Rebrandlymake · $3.99 · Uses make
    • Send custom emails and write files via SFTP with Make modulesmake · $3.99 · Uses make
    • Verify meeting compliance with MeetGeek and OpenAI to get alerts in Slackmake · $4.99 · Uses make
    • Create a Revolut Business draft payment for shipping and send an Email notification from a new Shopify ordermake · $4.99 · Uses make
    Browse all workflows