Next.js has revolutionized React development with its powerful features like App Router, Server Components, and built-in optimizations. When combined with Cursor's AI assistance and properly configured coding rules, you can dramatically accelerate your Next.js development workflow while maintaining high code quality and consistency.
This comprehensive guide provides battle-tested Cursor coding rules specifically designed for Next.js applications, covering everything from project structure to advanced optimization patterns.
Modern Next.js applications involve multiple paradigms:
Without proper guidance, AI assistants can generate code that mixes these paradigms incorrectly, leading to runtime errors, performance issues, and architectural inconsistencies.
š Development Acceleration Benefits
- Consistent Architecture: Ensures proper separation between server and client code
- Performance Optimization: Automatically applies Next.js best practices
- Type Safety: Enforces TypeScript patterns specific to Next.js
- SEO Optimization: Implements metadata and structured data correctly
- Error Prevention: Avoids common Next.js pitfalls and anti-patterns
Create or update your .cursorrules
file with these fundamental Next.js rules:
Basic Next.js Cursor Rules
# Next.js 14+ App Router Cursor Rules
## Project Context
This is a Next.js 14+ application using:
- App Router (not Pages Router)
- TypeScript for type safety
- Server Components by default
- Client Components only when needed
## Core Principles
1. Server Components are the default - use "use client" only when necessary
2. App Router file conventions must be followed exactly
3. Metadata API for SEO optimization
4. TypeScript interfaces for all props and data structures
App Router Structure Rules
## File Structure Rules
### App Directory Structure
- page.tsx: Route pages (Server Components by default)
- layout.tsx: Shared layouts with proper TypeScript typing
- loading.tsx: Loading UI with Suspense boundaries
- error.tsx: Error boundaries with proper error handling
- not-found.tsx: 404 pages with custom styling
- route.ts: API routes with proper HTTP methods
### Naming Conventions
- Components: PascalCase (UserProfile.tsx)
- Utilities: camelCase (formatDate.ts)
- Types: PascalCase with Type suffix (UserType.ts)
- Constants: UPPER_SNAKE_CASE (API_ENDPOINTS.ts)
### Import Organization
1. React and Next.js imports
2. Third-party libraries
3. Internal components
4. Utilities and helpers
5. Type imports (import type {...})
Component Classification Rules
## Server vs Client Component Rules
### Server Components (Default)
- Use for data fetching, static content, SEO-critical content
- No useState, useEffect, or browser APIs
- Can directly access databases and server-side resources
- Better performance and SEO
### Client Components ("use client")
- Use only for interactivity: forms, event handlers, state management
- Browser APIs: localStorage, geolocation, etc.
- Third-party libraries that require browser environment
- Real-time features: WebSocket connections
### Component Templates
Server Component:
```typescript
// Server Component (default)
interface PageProps {
params: { id: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export default async function Page({ params, searchParams }: PageProps) {
const data = await fetchData(params.id)
return (
<div>
<h1>{data.title}</h1>
<ServerChildComponent data={data} />
</div>
)
}
Client Component:
"use client"
import { useState, useEffect } from 'react'
interface Props {
initialData: DataType
}
export default function InteractiveComponent({ initialData }: Props) {
const [data, setData] = useState(initialData)
const handleClick = () => {
// Client-side interaction
}
return (
<button onClick={handleClick}>
{data.title}
</button>
)
}
### TypeScript Integration Rules
**Next.js TypeScript Patterns**
```markdown
## TypeScript Rules for Next.js
### Page Props Interface
```typescript
// For dynamic routes
interface PageProps {
params: Promise<{ slug: string }>
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}
// For static routes
interface PageProps {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}
interface LayoutProps {
children: React.ReactNode
params: Promise<{ slug: string }> // if dynamic
}
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
return NextResponse.json({ data: [] })
}
export async function POST(request: NextRequest) {
const body = await request.json()
return NextResponse.json({ success: true })
}
// error.tsx
interface ErrorProps {
error: Error & { digest?: string }
reset: () => void
}
export default function Error({ error, reset }: ErrorProps) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
)
}
## Advanced Next.js Cursor Rules
### Data Fetching Patterns
**Server-Side Data Fetching Rules**
```markdown
## Data Fetching Rules
### Server Components Data Fetching
- Use async/await directly in Server Components
- Implement proper error handling with try-catch
- Use fetch with Next.js cache options
- Type all API responses
### Data Fetching Template
```typescript
// Server Component data fetching
async function getData(id: string): Promise<DataType> {
try {
const res = await fetch(`${process.env.API_URL}/data/${id}`, {
next: { revalidate: 3600 } // Cache for 1 hour
})
if (!res.ok) {
throw new Error(`Failed to fetch data: ${res.status}`)
}
return res.json()
} catch (error) {
console.error('Data fetching error:', error)
throw new Error('Failed to load data')
}
}
export default async function Page({ params }: PageProps) {
const { id } = await params
const data = await getData(id)
return <DataDisplay data={data} />
}
export default async function Page() {
// Fetch data in parallel
const [userData, postsData, commentsData] = await Promise.all([
fetchUserData(),
fetchPosts(),
fetchComments()
])
return (
<div>
<UserProfile user={userData} />
<PostsList posts={postsData} />
<CommentsList comments={commentsData} />
</div>
)
}
### Metadata and SEO Rules
**SEO Optimization Rules**
```markdown
## Metadata and SEO Rules
### Static Metadata
```typescript
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Page Title | Site Name',
description: 'Comprehensive page description for SEO',
keywords: ['keyword1', 'keyword2', 'keyword3'],
authors: [{ name: 'Author Name' }],
openGraph: {
title: 'Page Title',
description: 'Page description',
images: ['/og-image.jpg'],
},
twitter: {
card: 'summary_large_image',
title: 'Page Title',
description: 'Page description',
images: ['/twitter-image.jpg'],
},
}
interface PageProps {
params: Promise<{ slug: string }>
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params
const post = await fetchPost(slug)
return {
title: `${post.title} | Blog`,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.featured_image],
},
}
}
export default function BlogPost({ post }: { post: PostType }) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.excerpt,
author: {
'@type': 'Person',
name: post.author.name,
},
datePublished: post.publishedAt,
dateModified: post.updatedAt,
}
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<article>{/* Post content */}</article>
</>
)
}
### API Routes and Server Actions
**API Development Rules**
```markdown
## API Routes Rules
### RESTful API Route Structure
```typescript
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
// Request validation schema
const CreatePostSchema = z.object({
title: z.string().min(1),
content: z.string().min(1),
tags: z.array(z.string()).optional(),
})
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '10')
const posts = await fetchPosts({ page, limit })
return NextResponse.json({
data: posts,
pagination: {
page,
limit,
total: posts.length
}
})
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch posts' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const validatedData = CreatePostSchema.parse(body)
const post = await createPost(validatedData)
return NextResponse.json(
{ data: post, message: 'Post created successfully' },
{ status: 201 }
)
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Validation failed', details: error.errors },
{ status: 400 }
)
}
return NextResponse.json(
{ error: 'Failed to create post' },
{ status: 500 }
)
}
}
// app/actions/posts.ts
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
try {
const title = formData.get('title') as string
const content = formData.get('content') as string
// Validate input
if (!title || !content) {
return { error: 'Title and content are required' }
}
// Create post
const post = await savePost({ title, content })
// Revalidate cache
revalidatePath('/posts')
// Redirect to new post
redirect(`/posts/${post.slug}`)
} catch (error) {
return { error: 'Failed to create post' }
}
}
### Performance Optimization Rules
**Next.js Performance Rules**
```markdown
## Performance Optimization Rules
### Image Optimization
```typescript
import Image from 'next/image'
// Always use Next.js Image component
export function OptimizedImage({ src, alt, ...props }: ImageProps) {
return (
<Image
src={src}
alt={alt}
width={800}
height={600}
priority={props.priority || false}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
{...props}
/>
)
}
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
})
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono',
})
export default function RootLayout({ children }: LayoutProps) {
return (
<html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
<body className="font-sans">{children}</body>
</html>
)
}
// Use dynamic imports for large components
import dynamic from 'next/dynamic'
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <div>Loading...</div>,
ssr: false, // Only if component doesn't need SSR
})
// Lazy load components that aren't immediately visible
const LazySection = dynamic(() => import('./LazySection'), {
loading: () => <div className="animate-pulse h-64 bg-gray-200"></div>,
})
// Static data that rarely changes
export async function getStaticData() {
const data = await fetch('https://api.example.com/static', {
next: { revalidate: 86400 } // 24 hours
})
return data.json()
}
// Dynamic data that changes frequently
export async function getDynamicData() {
const data = await fetch('https://api.example.com/dynamic', {
next: { revalidate: 60 } // 1 minute
})
return data.json()
}
// Real-time data
export async function getRealtimeData() {
const data = await fetch('https://api.example.com/realtime', {
cache: 'no-store'
})
return data.json()
}
## Complete Cursor Rules Template
### Production-Ready .cursorrules File
**Complete Next.js Cursor Rules Configuration**
```markdown
# Next.js 14+ App Router Production Rules
## Project Context
This is a Next.js 14+ production application using:
- App Router with TypeScript
- Server Components by default
- Client Components only when necessary
- Tailwind CSS for styling
- ESLint + Prettier for code quality
## Core Development Principles
### 1. Component Architecture
- Server Components are default (no "use client" unless needed)
- Client Components only for interactivity and browser APIs
- Proper TypeScript interfaces for all props
- Export default for main components, named exports for utilities
### 2. File Organization
app/ āāā (auth)/ # Route groups āāā api/ # API routes āāā globals.css # Global styles āāā layout.tsx # Root layout
components/ āāā ui/ # Reusable UI components āāā forms/ # Form components āāā sections/ # Page sections
lib/ āāā utils.ts # Utility functions āāā validations.ts # Zod schemas āāā constants.ts # App constants
types/ āāā index.ts # TypeScript types
### 3. TypeScript Standards
```typescript
// Page component interface
interface PageProps {
params: Promise<{ slug: string }>
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}
// Layout component interface
interface LayoutProps {
children: React.ReactNode
params?: Promise<{ slug: string }>
}
// API response type
interface ApiResponse<T> {
data?: T
error?: string
message?: string
}
// Async server component with error handling
export default async function ServerPage({ params }: PageProps) {
try {
const { slug } = await params
const data = await fetchData(slug)
return (
<main>
<h1>{data.title}</h1>
<ContentSection data={data} />
</main>
)
} catch (error) {
notFound()
}
}
// Parallel data fetching
export default async function DashboardPage() {
const [user, posts, analytics] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchAnalytics()
])
return (
<Dashboard user={user} posts={posts} analytics={analytics} />
)
}
"use client"
import { useState, useEffect } from 'react'
interface ClientComponentProps {
initialData: DataType
}
export default function ClientComponent({ initialData }: ClientComponentProps) {
const [data, setData] = useState(initialData)
const [loading, setLoading] = useState(false)
const handleUpdate = async () => {
setLoading(true)
try {
const response = await fetch('/api/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
if (!response.ok) throw new Error('Update failed')
const result = await response.json()
setData(result.data)
} catch (error) {
console.error('Update error:', error)
} finally {
setLoading(false)
}
}
return (
<div>
<button onClick={handleUpdate} disabled={loading}>
{loading ? 'Updating...' : 'Update'}
</button>
</div>
)
}
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
const PostSchema = z.object({
title: z.string().min(1),
content: z.string().min(1),
published: z.boolean().default(false)
})
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const posts = await fetchPosts({ page })
return NextResponse.json({ data: posts })
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch posts' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const validatedData = PostSchema.parse(body)
const post = await createPost(validatedData)
return NextResponse.json(
{ data: post, message: 'Post created' },
{ status: 201 }
)
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Validation failed', details: error.errors },
{ status: 400 }
)
}
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
import type { Metadata } from 'next'
// Static metadata
export const metadata: Metadata = {
title: 'Page Title | Site Name',
description: 'SEO-optimized description',
openGraph: {
title: 'Page Title',
description: 'Page description',
images: ['/og-image.jpg'],
},
}
// Dynamic metadata
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params
const post = await fetchPost(slug)
return {
title: `${post.title} | Blog`,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.image],
},
}
}
// error.tsx
interface ErrorProps {
error: Error & { digest?: string }
reset: () => void
}
export default function Error({ error, reset }: ErrorProps) {
useEffect(() => {
console.error('Page error:', error)
}, [error])
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h2 className="text-2xl font-bold mb-4">Something went wrong!</h2>
<button
onClick={reset}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Try again
</button>
</div>
)
}
// not-found.tsx
export default function NotFound() {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h2 className="text-2xl font-bold mb-4">Page Not Found</h2>
<p className="text-gray-600 mb-4">Could not find requested resource</p>
<Link href="/" className="text-blue-500 hover:underline">
Return Home
</Link>
</div>
)
}
// loading.tsx
export default function Loading() {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500"></div>
</div>
)
}
// Component-level loading
export function LoadingCard() {
return (
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
</div>
)
}
## Testing and Validation
### Rule Effectiveness Testing
**Testing Your Cursor Rules**
```markdown
## Rule Testing Scenarios
### Test 1: Page Component Creation
Prompt: "Create a blog post page with dynamic routing"
Expected Result:
- Server Component with proper PageProps interface
- Async function with params destructuring
- Error handling with try-catch and notFound()
- Proper metadata generation
- TypeScript interfaces for data structures
### Test 2: Client Component Creation
Prompt: "Create an interactive comment form"
Expected Result:
- "use client" directive at the top
- Proper form handling with state management
- Error handling for API calls
- Loading states during submission
- TypeScript interfaces for form data
### Test 3: API Route Creation
Prompt: "Create an API route for user authentication"
Expected Result:
- Proper HTTP method handlers (GET, POST, etc.)
- Request validation with Zod or similar
- Error responses with appropriate status codes
- TypeScript typing for request/response
- Proper error handling structure
### Test 4: Layout Component Creation
Prompt: "Create a dashboard layout with navigation"
Expected Result:
- Server Component with LayoutProps interface
- Proper children prop handling
- Metadata configuration
- Navigation component integration
- Responsive design patterns
Troubleshooting Next.js Cursor Rules
ā ļø Common Problems and Fixes
Problem: Cursor generates Client Components when Server Components are needed Solution: Emphasize "Server Components by default" rule and specify when to use "use client"
Problem: Incorrect TypeScript interfaces for Next.js specific props Solution: Provide explicit interface templates for PageProps, LayoutProps, etc.
Problem: Mixed App Router and Pages Router patterns Solution: Clearly specify "App Router only" and provide App Router specific examples
Problem: Inconsistent API route patterns Solution: Provide comprehensive API route templates with proper error handling
Environment-Aware Rules
## Environment-Specific Rules
### Development Environment
- Enable detailed error logging
- Use development-specific debugging tools
- Allow console.log statements for debugging
- Include source maps and verbose error messages
### Production Environment
- Implement comprehensive error handling
- Use structured logging instead of console.log
- Enable all performance optimizations
- Include proper monitoring and analytics
### Testing Environment
- Mock external dependencies
- Use test-specific configurations
- Implement proper test data factories
- Include coverage requirements
Multi-Developer Consistency
## Team Collaboration Standards
### Code Review Checklist
- Server/Client component usage is appropriate
- TypeScript interfaces are properly defined
- Error handling is implemented consistently
- Performance considerations are addressed
- SEO metadata is properly configured
### Shared Patterns
- Use shared TypeScript types from types/ directory
- Follow consistent naming conventions
- Implement shared utility functions
- Use standardized error handling patterns
### Documentation Requirements
- JSDoc comments for complex functions
- README updates for new features
- API documentation for new endpoints
- Component documentation with examples
Track these metrics to measure the effectiveness of your Cursor rules:
š Success Indicators
Code Quality Metrics
- TypeScript error reduction
- ESLint warning decrease
- Consistent component patterns
- Proper error handling implementation
Performance Metrics
- Core Web Vitals improvements
- Bundle size optimization
- Loading speed enhancements
- SEO score improvements
Developer Experience
- Reduced debugging time
- Faster feature development
- Consistent code generation
- Better AI assistance accuracy
Rule Evolution Strategy
## Rule Maintenance Plan
### Weekly Reviews
- Check AI-generated code quality
- Identify pattern inconsistencies
- Update rules based on new requirements
- Test rule effectiveness with common prompts
### Monthly Updates
- Review Next.js updates and new features
- Update TypeScript patterns for new versions
- Optimize rules for performance
- Gather team feedback on rule effectiveness
### Quarterly Overhauls
- Major rule architecture reviews
- Integration with new tools and frameworks
- Performance analysis and optimization
- Documentation updates and examples
Effective Cursor coding rules for Next.js applications can dramatically improve your development workflow, code quality, and team consistency. The key is to:
šÆ Key Success Factors
- Specificity: Detailed rules work better than general guidelines
- Context Awareness: Rules should understand Next.js paradigms
- Error Prevention: Focus on preventing common Next.js mistakes
- Performance Focus: Embed optimization patterns in rules
- Type Safety: Ensure TypeScript integration throughout
By implementing these comprehensive Cursor coding rules for Next.js apps, you'll create a development environment where AI assistance consistently generates high-quality, performant, and maintainable code that follows Next.js best practices.
Remember that these rules are living documents ā they should evolve with your project needs, team growth, and Next.js framework updates. Regular review and refinement will ensure your Cursor rules continue to provide maximum value throughout your development journey.
This guide represents the latest best practices for Next.js development with Cursor AI assistance. As Next.js continues to evolve, these patterns and rules will be updated to reflect the most current and effective approaches.
Expert developer passionate about modern web technologies and AI-assisted development.
Get the latest articles and tutorials delivered to your inbox.