Cursor rule
example-structures/next-js/.cursor/rules/app-router-patterns.mdcNext.js 14+ App Router patterns — Server Components, Client Components, Route Handlers, Server Actions, and metadata API
Cursor rules
Quality
65/100
Scores the file, not the repository.Length
1,273 words
9 headings · 9 code blocksRepository
18
— · pushed 0 days agoLast changed
2 days ago
First indexed 2 days ago.123456# Next.js App Router Excellence78## Server Components vs Client Components910- **Default is Server Component** — every file in `app/` is a Server Component unless marked otherwise11- Add `"use client"` only at the boundary where you need browser APIs, event handlers, or React state12- Keep `"use client"` components as leaf nodes; wrap them in Server Components for data fetching13- Never fetch data inside a Client Component when a Server Component parent can pass it as a prop1415```tsx16// ✅ Server Component — async, direct DB/API access, no "use client"17// app/products/page.tsx18import { db } from '@/lib/db'1920export default async function ProductsPage() {21 const products = await db.product.findMany({ orderBy: { createdAt: 'desc' } })2223 return (24 <main>25 <h1>Products</h1>26 {products.map((product) => (27 <ProductCard key={product.id} product={product} />28 ))}29 </main>30 )31}3233// ✅ Client Component — only for interactivity34// components/add-to-cart-button.tsx35'use client'3637import { useState } from 'react'3839interface AddToCartButtonProps {40 productId: string41}4243export function AddToCartButton({ productId }: AddToCartButtonProps) {44 const [loading, setLoading] = useState(false)4546 const handleAdd = async () => {47 setLoading(true)48 await addToCart(productId)49 setLoading(false)50 }5152 return (53 <button onClick={handleAdd} disabled={loading}>54 {loading ? 'Adding…' : 'Add to Cart'}55 </button>56 )57}58```5960## App Router File Conventions6162```63app/64├── layout.tsx # Root layout — wraps all routes, runs once65├── page.tsx # Route UI rendered at /66├── loading.tsx # Automatic Suspense boundary for this segment67├── error.tsx # Error boundary ("use client" required)68├── not-found.tsx # Rendered by notFound() or unmatched routes69├── global-error.tsx # Error boundary for root layout70├── route.ts # API Route Handler (no page.tsx in same segment)71├── template.tsx # Like layout but re-mounts on navigation72└── products/73 ├── page.tsx # Renders at /products74 ├── [id]/75 │ └── page.tsx # Renders at /products/:id76 └── (marketing)/ # Route group — ignored in URL path77 └── page.tsx78```7980```tsx81// ✅ Root layout — must return <html> and <body>82// app/layout.tsx83import type { Metadata } from 'next'84import { Inter } from 'next/font/google'8586const inter = Inter({ subsets: ['latin'] })8788export const metadata: Metadata = {89 title: { template: '%s | My App', default: 'My App' },90 description: 'My application description',91}9293export default function RootLayout({ children }: { children: React.ReactNode }) {94 return (95 <html lang="en">96 <body className={inter.className}>{children}</body>97 </html>98 )99}100101// ✅ Error boundary — must be a Client Component102// app/products/error.tsx103'use client'104105export default function Error({106 error,107 reset,108}: {109 error: Error & { digest?: string }110 reset: () => void111}) {112 return (113 <div>114 <h2>Something went wrong</h2>115 <button onClick={reset}>Try again</button>116 </div>117 )118}119```120121## Route Handlers122123Use `route.ts` for API endpoints. Export named functions for each HTTP method.124125```typescript126// ✅ app/api/products/route.ts127import { NextRequest, NextResponse } from 'next/server'128import { z } from 'zod'129import { db } from '@/lib/db'130131export async function GET(request: NextRequest) {132 const { searchParams } = new URL(request.url)133 const page = Number(searchParams.get('page') ?? '1')134135 const products = await db.product.findMany({136 skip: (page - 1) * 20,137 take: 20,138 })139140 return NextResponse.json({ products })141}142143const createProductSchema = z.object({144 name: z.string().min(1),145 price: z.number().positive(),146})147148export async function POST(request: NextRequest) {149 const body = await request.json()150 const parsed = createProductSchema.safeParse(body)151152 if (!parsed.success) {153 return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 })154 }155156 const product = await db.product.create({ data: parsed.data })157 return NextResponse.json(product, { status: 201 })158}159160// ✅ Dynamic route handler: app/api/products/[id]/route.ts161export async function GET(162 request: NextRequest,163 { params }: { params: Promise<{ id: string }> }164) {165 const { id } = await params166 const product = await db.product.findUnique({ where: { id } })167168 if (!product) {169 return NextResponse.json({ error: 'Not found' }, { status: 404 })170 }171172 return NextResponse.json(product)173}174```175176## Server Actions177178Use Server Actions for mutations triggered from forms or Client Components.179180```typescript181// ✅ lib/actions/product.ts182'use server'183184import { revalidatePath } from 'next/cache'185import { redirect } from 'next/navigation'186import { z } from 'zod'187import { db } from '@/lib/db'188189const schema = z.object({190 name: z.string().min(1, 'Name is required'),191 price: z.coerce.number().positive('Price must be positive'),192})193194export async function createProduct(prevState: unknown, formData: FormData) {195 const parsed = schema.safeParse({196 name: formData.get('name'),197 price: formData.get('price'),198 })199200 if (!parsed.success) {201 return { errors: parsed.error.flatten().fieldErrors }202 }203204 await db.product.create({ data: parsed.data })205206 revalidatePath('/products')207 redirect('/products')208}209```210211```tsx212// ✅ Using Server Action with useActionState213// app/products/new/page.tsx214'use client'215216import { useActionState } from 'react'217import { createProduct } from '@/lib/actions/product'218219export default function NewProductPage() {220 const [state, action, pending] = useActionState(createProduct, null)221222 return (223 <form action={action}>224 <div>225 <label htmlFor="name">Name</label>226 <input id="name" name="name" type="text" required />227 {state?.errors?.name && <p>{state.errors.name[0]}</p>}228 </div>229 <div>230 <label htmlFor="price">Price</label>231 <input id="price" name="price" type="number" step="0.01" required />232 {state?.errors?.price && <p>{state.errors.price[0]}</p>}233 </div>234 <button type="submit" disabled={pending}>235 {pending ? 'Creating…' : 'Create Product'}236 </button>237 </form>238 )239}240```241242## Metadata API243244```tsx245// ✅ Static metadata246// app/about/page.tsx247import type { Metadata } from 'next'248249export const metadata: Metadata = {250 title: 'About Us',251 description: 'Learn more about our company',252 openGraph: {253 title: 'About Us',254 description: 'Learn more about our company',255 images: ['/og-about.png'],256 },257}258259// ✅ Dynamic metadata from route params260// app/products/[id]/page.tsx261import type { Metadata } from 'next'262import { db } from '@/lib/db'263264interface Props {265 params: Promise<{ id: string }>266}267268export async function generateMetadata({ params }: Props): Promise<Metadata> {269 const { id } = await params270 const product = await db.product.findUnique({ where: { id } })271272 if (!product) return { title: 'Product Not Found' }273274 return {275 title: product.name,276 description: product.description,277 openGraph: { images: [product.imageUrl] },278 }279}280281export default async function ProductPage({ params }: Props) {282 const { id } = await params283 const product = await db.product.findUnique({ where: { id } })284 // ...285}286```287288## Data Fetching Patterns289290```tsx291// ✅ Parallel data fetching in Server Components292export default async function DashboardPage() {293 const [user, stats, notifications] = await Promise.all([294 fetchUser(),295 fetchStats(),296 fetchNotifications(),297 ])298299 return <Dashboard user={user} stats={stats} notifications={notifications} />300}301302// ✅ Streaming with Suspense303import { Suspense } from 'react'304305export default function Page() {306 return (307 <main>308 <h1>Dashboard</h1>309 <Suspense fallback={<StatsSkeleton />}>310 <StatsPanel /> {/* async Server Component */}311 </Suspense>312 <Suspense fallback={<FeedSkeleton />}>313 <ActivityFeed /> {/* async Server Component */}314 </Suspense>315 </main>316 )317}318319// ✅ fetch() with Next.js cache options320async function getProduct(id: string) {321 const res = await fetch(`https://api.example.com/products/${id}`, {322 next: { revalidate: 3600 }, // ISR: revalidate every hour323 })324325 if (!res.ok) throw new Error('Failed to fetch product')326 return res.json()327}328329// Force dynamic (no cache)330async function getLivePrice(symbol: string) {331 const res = await fetch(`https://api.example.com/price/${symbol}`, {332 cache: 'no-store',333 })334 return res.json()335}336```337338## Image and Font Optimization339340```tsx341// ✅ next/image — always specify width/height or fill342import Image from 'next/image'343344// Fixed dimensions345<Image src="/hero.png" alt="Hero banner" width={1200} height={600} priority />346347// Fill parent container348<div className="relative h-64 w-full">349 <Image src={product.imageUrl} alt={product.name} fill className="object-cover" />350</div>351352// ✅ next/font — load at module level, not inside components353import { Inter, Roboto_Mono } from 'next/font/google'354355const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })356const robotoMono = Roboto_Mono({ subsets: ['latin'], variable: '--font-mono' })357358export default function RootLayout({ children }: { children: React.ReactNode }) {359 return (360 <html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>361 <body>{children}</body>362 </html>363 )364}365```366367## Key Rules368369- Params are now `Promise<{ ... }>` in Next.js 15 — always `await params`370- Never use `pages/` and `app/` for the same routes — pick one per segment371- `revalidatePath()` and `revalidateTag()` only work in Server Actions and Route Handlers372- Use `next/navigation` (`useRouter`, `redirect`, `notFound`) in App Router, not `next/router`373- Co-locate non-route files (components, utils) inside `app/` using `_` prefix or route groups `(group)` to prevent them from becoming routes374
Also in tugkanboz/awesome-cursorrules
Diff this repo’s formatsOne repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| tugkanboz/awesome-cursorrulesexample-structures/cypress/.cursor/rules/api-testing.mdc · 18 | Cursor rules | testtesting-strategyapi | 58/100 | 2 days ago | |
| tugkanboz/awesome-cursorrulesexample-structures/cypress/.cursor/rules/testing-fundamentals.mdc · 18 | Cursor rules | testarchtesting-strategysecurity+2 | 62/100 | 2 days ago | |
| tugkanboz/awesome-cursorrulesrules/appium-mobile-test-automation-framework/.cursorrules · 18 | .cursorrules | teststylearchperformance+2 | 56/100 | 2 days ago | |
| tugkanboz/awesome-cursorrulesrules/cypress-javascript-test-automation-framework/.cursorrules · 18 | .cursorrules | testlint-formatstylearch+4 | 59/100 | 2 days ago | |
| tugkanboz/awesome-cursorrulesrules/k6-performance-test-framework/.cursorrules · 18 | .cursorrules | setupteststylearch+2 | 56/100 | 2 days ago | |
| tugkanboz/awesome-cursorrulesrules/restassured-java-framework/.cursorrules · 18 | .cursorrules | teststylearchsecurity+4 | 56/100 | 2 days ago | |
| tugkanboz/awesome-cursorrulesrules/selenium-net-test-automation-framework/.cursorrules · 18 | .cursorrules | teststylearchdeployment+1 | 56/100 | 2 days ago | |
| tugkanboz/awesome-cursorrulesrules/selenium-python-test-automation-framework/.cursorrules · 18 | .cursorrules | testlint-formatstylearch+3 | 59/100 | 2 days ago | |
| tugkanboz/awesome-cursorrulesrules/webdriverio-javascript-test-automation-framework/.cursorrules · 18 | .cursorrules | testlint-formatstylearch+4 | 69/100 | 2 days ago | |
| tugkanboz/awesome-cursorrulesexample-structures/react-typescript/.cursor/rules/component-development.mdc · 18 | Cursor rules | teststylearchtypes+1 | 58/100 | 2 days ago | |
| tugkanboz/awesome-cursorrulesexample-structures/selenium-python/.cursor/rules/framework-architecture.mdc · 18 | Cursor rules | setuptestlint-formatstyle+3 | 93/100 | 2 days ago | |
| tugkanboz/awesome-cursorrulesexample-structures/selenium-python/.cursor/rules/page-object-patterns.mdc · 18 | Cursor rules | ui | 54/100 | 2 days ago | |
| tugkanboz/awesome-cursorrulesexample-structures/selenium-python/.cursor/rules/test-patterns.mdc · 18 | Cursor rules | teststylearchtesting-strategy+1 | 66/100 | 2 days ago | |
| tugkanboz/awesome-cursorrulesframeworks/cypress/.cursor/rules/cypress-excellence.mdc · 18 | Cursor rules | testtesting-strategysecurityperformance+1 | 58/100 | 2 days ago | |
| tugkanboz/awesome-cursorrulesrules/playwright-javascript-test-automation-framework/.cursorrules · 18 | .cursorrules | testlint-formatstylearch+3 | 59/100 | 2 days ago | |
| tugkanboz/awesome-cursorrulesrules/vitest-javascript-unit-test-framework/.cursorrules · 18 | .cursorrules | setupteststylearch+5 | 60/100 | 2 days ago |
Diff against example-structures/cypress/.cursor/rules/api-testing.mdc Diff against example-structures/cypress/.cursor/rules/testing-fundamentals.mdc Diff against rules/appium-mobile-test-automation-framework/.cursorrules Diff against rules/cypress-javascript-test-automation-framework/.cursorrules Diff against rules/k6-performance-test-framework/.cursorrules Diff against rules/restassured-java-framework/.cursorrules Diff against rules/selenium-net-test-automation-framework/.cursorrules Diff against rules/selenium-python-test-automation-framework/.cursorrules Diff against rules/webdriverio-javascript-test-automation-framework/.cursorrules Diff against example-structures/react-typescript/.cursor/rules/component-development.mdc Diff against example-structures/selenium-python/.cursor/rules/framework-architecture.mdc Diff against example-structures/selenium-python/.cursor/rules/page-object-patterns.mdc Diff against example-structures/selenium-python/.cursor/rules/test-patterns.mdc Diff against frameworks/cypress/.cursor/rules/cypress-excellence.mdc Diff against rules/playwright-javascript-test-automation-framework/.cursorrules Diff against rules/vitest-javascript-unit-test-framework/.cursorrules
