Instruction file imported from ac7x/lin-llc (
.cursor/rules/nextjs.mdc). Copyright stays with the author.
rule_type: auto_attached title: Next.js Development Rules description: Next.js App Router patterns and best practices patterns: ["/app//.{ts,tsx}", "/pages//.{ts,tsx}", "next.config.*", "**/middleware.{ts,tsx}"]
Next.js Development Rules
App Router Structure
File-based Routing
app/
├── globals.css
├── layout.tsx # Root layout
├── page.tsx # Home page
├── loading.tsx # Loading UI
├── error.tsx # Error UI
├── not-found.tsx # 404 page
├── dashboard/
│ ├── layout.tsx # Nested layout
│ ├── page.tsx # /dashboard
│ ├── settings/
│ │ └── page.tsx # /dashboard/settings
│ └── [id]/
│ └── page.tsx # /dashboard/[id]
└── api/
└── users/
└── route.ts # API endpoints
Page Components
// app/page.tsx - Server Component by default
export default function HomePage() {
return (
<div>
<h1>Welcome</h1>
</div>
)
}
// With metadata
export const metadata = {
title: 'Home',
description: 'Welcome to our app',
}
// Dynamic route with params
// app/posts/[slug]/page.tsx
interface PageProps {
params: { slug: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export default function PostPage({ params, searchParams }: PageProps) {
return <div>Post: {params.slug}</div>
}
Layouts
// app/layout.tsx - Root Layout
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<nav>Navigation</nav>
<main>{children}</main>
<footer>Footer</footer>
</body>
</html>
)
}
// Nested layout
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="dashboard">
<aside>Sidebar</aside>
<div>{children}</div>
</div>
)
}
Data Fetching
Server Components (Default)
// Fetch data directly in server components
async function getData() {
const res = await fetch('https://api.example.com/data', {
cache: 'force-cache', // SSG
// cache: 'no-store', // SSR
// next: { revalidate: 60 } // ISR
})
if (!res.ok) {
throw new Error('Failed to fetch data')
}
return res.json()
}
export default async function Page() {
const data = await getData()
return (
<div>
{data.map((item: any) => (
<div key={item.id}>{item.name}</div>
))}
</div>
)
}
Streaming with Suspense
// app/dashboard/page.tsx
import { Suspense } from 'react'
import { Posts } from './posts'
import { Analytics } from './analytics'
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading posts...</div>}>
<Posts />
</Suspense>
<Suspense fallback={<div>Loading analytics...</div>}>
<Analytics />
</Suspense>
</div>
)
}
// Slow component that fetches data
async function Posts() {
await new Promise(resolve => setTimeout(resolve, 2000))
const posts = await fetch('/api/posts').then(res => res.json())
return (
<div>
{posts.map((post: any) => (
<div key={post.id}>{post.title}</div>
))}
</div>
)
}
Client Components
'use client'
import { useState, useEffect } from 'react'
export default function ClientComponent() {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => {
setData(data)
setLoading(false)
})
}, [])
if (loading) return <div>Loading...</div>
return <div>{JSON.stringify(data)}</div>
}
API Routes
Basic API Route
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const query = searchParams.get('query')
const users = await fetchUsers(query)
return NextResponse.json({ users })
}
export async function POST(request: NextRequest) {
const body = await request.json()
try {
const user = await createUser(body)
return NextResponse.json({ user }, { status: 201 })
} catch (error) {
return NextResponse.json(
{ error: 'Failed to create user' },
{ status: 500 }
)
}
}
Dynamic API Routes
// app/api/users/[id]/route.ts
interface Context {
params: { id: string }
}
export async function GET(
request: NextRequest,
{ params }: Context
) {
const user = await getUserById(params.id)
if (!user) {
return NextResponse.json(
{ error: 'User not found' },
{ status: 404 }
)
}
return NextResponse.json({ user })
}
export async function PUT(
request: NextRequest,
{ params }: Context
) {
const body = await request.json()
const updatedUser = await updateUser(params.id, body)
return NextResponse.json({ user: updatedUser })
}
export async function DELETE(
request: NextRequest,
{ params }: Context
) {
await deleteUser(params.id)
return NextResponse.json({ success: true })
}
API Route with Validation
import { z } from 'zod'
const CreateUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().min(0).max(120),
})
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const validatedData = CreateUserSchema.parse(body)
const user = await createUser(validatedData)
return NextResponse.json({ user }, { 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 }
)
}
}
Middleware
Basic Middleware
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Check authentication
if (request.nextUrl.pathname.startsWith('/dashboard')) {
const token = request.cookies.get('token')
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
// Add custom headers
const response = NextResponse.next()
response.headers.set('x-custom-header', 'my-value')
return response
}
export const config = {
matcher: [
'/dashboard/:path*',
'/api/:path*',
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}
Advanced Middleware with JWT
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { jwtVerify } from 'jose'
const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET)
export async function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/api/protected')) {
const token = request.headers.get('authorization')?.replace('Bearer ', '')
if (!token) {
return NextResponse.json({ error: 'No token' }, { status: 401 })
}
try {
await jwtVerify(token, JWT_SECRET)
} catch {
return NextResponse.json({ error: 'Invalid token' }, { status: 401 })
}
}
return NextResponse.next()
}
Server Actions
Form Actions
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
// Validate
if (!title || !content) {
throw new Error('Title and content are required')
}
// Save to database
await savePost({ title, content })
// Revalidate and redirect
revalidatePath('/posts')
redirect('/posts')
}
// In component
export default function CreatePostForm() {
return (
<form action={createPost}>
<input name="title" placeholder="Title" required />
<textarea name="content" placeholder="Content" required />
<button type="submit">Create Post</button>
</form>
)
}
Server Actions with State
'use server'
import { z } from 'zod'
const PostSchema = z.object({
title: z.string().min(1),
content: z.string().min(1),
})
type State = {
errors?: {
title?: string[]
content?: string[]
}
message?: string
}
export async function createPost(
prevState: State,
formData: FormData
): Promise<State> {
const validatedFields = PostSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
})
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Missing fields',
}
}
try {
await savePost(validatedFields.data)
revalidatePath('/posts')
return { message: 'Post created successfully' }
} catch (error) {
return { message: 'Failed to create post' }
}
}
Performance Optimization
Image Optimization
import Image from 'next/image'
// Responsive images
<Image
src="/hero.jpg"
alt="Hero image"
width={800}
height={600}
priority // Load immediately
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
// Fill container
<div style={{ position: 'relative', width: '100%', height: '400px' }}>
<Image
src="/background.jpg"
alt="Background"
fill
style={{ objectFit: 'cover' }}
/>
</div>
Dynamic Imports
import dynamic from 'next/dynamic'
// Lazy load component
const DynamicComponent = dynamic(() => import('../components/Heavy'), {
loading: () => <div>Loading...</div>,
ssr: false, // Client-side only
})
// Conditional loading
const AdminPanel = dynamic(() => import('../components/AdminPanel'), {
loading: () => <div>Loading admin panel...</div>,
})
export default function Dashboard({ isAdmin }: { isAdmin: boolean }) {
return (
<div>
{isAdmin && <AdminPanel />}
</div>
)
}
Bundle Analysis
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
bundlePagesRouterDependencies: true,
},
webpack: (config, { dev, isServer }) => {
if (!dev && !isServer) {
config.resolve.alias = {
...config.resolve.alias,
'@/lib/utils': '@/lib/utils/index.ts',
}
}
return config
},
}
// Bundle analyzer
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer(nextConfig)
Best Practices
- Use Server Components by default, Client Components only when needed
- Implement proper error boundaries and loading states
- Use TypeScript for better development experience
- Optimize images with next/image
- Implement proper caching strategies
- Use Server Actions for form submissions
- Keep client bundles small with dynamic imports
- Implement proper SEO with metadata
- Use middleware for authentication and redirects
- Monitor performance with Next.js Analytics