The Next.js Best Practices skill provides comprehensive guidance for building applications with Next.js App Router, covering file conventions, React Server Components, data fetching patterns, and framework-specific optimizations.
Overview
This skill covers:
File Conventions - Project structure and special files
RSC Boundaries - Server/client component patterns
Data Patterns - Server Components vs Server Actions vs Route Handlers
Async APIs - Next.js 15/16 async request APIs
This skill includes Next.js 15/16 specific guidance, including async request APIs (cookies(), headers(), params). The demo app uses Next.js 16.1.6.
File Conventions
Special Files
Next.js App Router uses file-based routing with special conventions:
File Purpose page.tsxUI for a route segment layout.tsxShared UI for segment and children loading.tsxLoading UI (Suspense boundary) error.tsxError UI (Error boundary) not-found.tsx404 UI route.tsAPI endpoint template.tsxLike layout but re-renders default.tsxFallback for parallel routes
Route Segments
Static
Dynamic
Catch-all
Optional Catch-all
Route Group
app/blog/page.tsx → /blog
Standard static route segment. app/blog/[slug]/page.tsx → /blog/:slug
Dynamic segment captures route parameter. app/docs/[...slug]/page.tsx → /docs/a/b/c
Matches multiple segments. app/shop/[[...slug]]/page.tsx → / or /shop/a/b
Matches root and multiple segments. app/(marketing)/about/page.tsx → /about
Organizes routes without affecting URL.
Parallel & Intercepting Routes
Parallel Routes
Intercepting Routes
// app/layout.tsx
export default function Layout ({
children ,
analytics ,
sidebar
} : {
children : React . ReactNode
analytics : React . ReactNode
sidebar : React . ReactNode
}) {
return (
<>
{ children }
{ analytics }
{ sidebar }
</>
)
}
// app/@analytics/page.tsx
// app/@sidebar/page.tsx
Data Fetching Patterns
Decision Tree
Need to fetch data?
├── From Server Component?
│ └── Use: Fetch directly (no API needed)
│
├── From Client Component?
│ ├── Mutation (POST/PUT/DELETE)?
│ │ └── Use: Server Action
│ └── Read (GET)?
│ └── Use: Route Handler OR pass from Server Component
│
├── External API access (webhooks)?
│ └── Use: Route Handler
│
└── REST API for mobile/external?
└── Use: Route Handler
Pattern 1: Server Components (Preferred)
Direct Database Access
External API
// app/users/page.tsx
async function UsersPage () {
// Direct database access - no API round-trip
const users = await db . user . findMany ()
return (
< ul >
{ users . map ( user => (
< li key = { user . id } > { user . name } </ li >
)) }
</ ul >
)
}
Benefits:
No API to maintain
No client-server waterfall
Secrets stay on server
Direct database access
Pattern 2: Server Actions (Mutations)
app/actions.ts
app/posts/new/page.tsx
'use server'
import { revalidatePath } from 'next/cache'
import { z } from 'zod'
const createPostSchema = z . object ({
title: z . string (). min ( 1 ). max ( 100 ),
content: z . string (). min ( 1 )
})
export async function createPost ( formData : FormData ) {
// 1. Validate input
const validated = createPostSchema . parse ({
title: formData . get ( 'title' ),
content: formData . get ( 'content' )
})
// 2. Authenticate
const session = await auth ()
if ( ! session ) {
throw new Error ( 'Unauthorized' )
}
// 3. Perform mutation
await db . post . create ({
data: {
... validated ,
authorId: session . user . id
}
})
// 4. Revalidate
revalidatePath ( '/posts' )
}
Benefits:
End-to-end type safety
Progressive enhancement (works without JS)
Automatic request handling
Integrated with React transitions
Constraints:
POST only (no GET caching semantics)
Internal use only (no external access)
Cannot return non-serializable data
Pattern 3: Route Handlers (APIs)
app/api/posts/route.ts
Dynamic Routes
import { NextRequest , NextResponse } from 'next/server'
// GET is cacheable
export async function GET ( request : NextRequest ) {
const posts = await db . post . findMany ()
return NextResponse . json ( posts )
}
// POST for mutations
export async function POST ( request : NextRequest ) {
const body = await request . json ()
const post = await db . post . create ({ data: body })
return NextResponse . json ( post , { status: 201 })
}
When to use:
External API access (mobile apps, third parties)
Webhooks from external services
GET endpoints that need HTTP caching
OpenAPI/Swagger documentation needed
When NOT to use:
Internal data fetching (use Server Components)
Mutations from your UI (use Server Actions)
RSC Boundaries
Invalid Patterns
❌ Async Client Component
✅ Pass Data from Server
'use client'
// This will error - client components cannot be async
export default async function ClientComponent () {
const data = await fetchData ()
return < div > { data } </ div >
}
Non-Serializable Props
❌ Cannot Pass Functions
✅ Define in Client Component
// Server Component
function ServerComponent () {
const handleClick = () => console . log ( 'clicked' )
return < ClientComponent onClick = { handleClick } />
// Error: Functions are not serializable
}
Async APIs (Next.js 15/16)
Next.js 15 and 16 use async APIs for request-specific data for performance reasons.
export default function Page ({ params } : Props ) {
const { id } = params
return < div > { id } </ div >
}
export default async function Page ({ params } : Props ) {
const { id } = await params
return < div > { id } </ div >
}
export default function Page ({ searchParams } : Props ) {
const { query } = searchParams
return < div > { query } </ div >
}
export default async function Page ({ searchParams } : Props ) {
const { query } = await searchParams
return < div > { query } </ div >
}
Run npx @next/codemod@latest upgrade to automatically migrate async APIs.
Image & Font Optimization
next/image
❌ Native img Tag
✅ next/image
Remote Images
< img src = "/profile.jpg" alt = "Profile" />
// No optimization, no lazy loading
next/font
import { Inter , Roboto_Mono } from 'next/font/google'
const inter = Inter ({
subsets: [ 'latin' ],
display: 'swap'
})
const robotoMono = Roboto_Mono ({
subsets: [ 'latin' ],
display: 'swap'
})
export default function RootLayout ({ children }) {
return (
< html className = { inter . className } >
< body > { children } </ body >
</ html >
)
}
Error Handling
error.tsx
not-found.tsx
Auth Errors
'use client'
export default function Error ({
error ,
reset
} : {
error : Error & { digest ?: string }
reset : () => void
}) {
return (
< div >
< h2 > Something went wrong! </ h2 >
< button onClick = { reset } > Try again </ button >
</ div >
)
}
export default function NotFound () {
return (
< div >
< h2 > Not Found </ h2 >
< p > Could not find requested resource </ p >
</ div >
)
}
import { notFound } from 'next/navigation'
async function Page ({ params }) {
const { id } = await params
const post = await db . post . findUnique ({ where: { id } })
if ( ! post ) {
notFound ()
}
return < div > { post . title } </ div >
}
import { forbidden , unauthorized } from 'next/navigation'
async function AdminPage () {
const session = await auth ()
if ( ! session ) {
unauthorized () // 401 error
}
if ( session . user . role !== 'admin' ) {
forbidden () // 403 error
}
return < AdminDashboard />
}
When to Use This Skill
Load this skill when:
Setting up new Next.js projects
Choosing between Server Components, Server Actions, and Route Handlers
Implementing data fetching patterns
Optimizing images and fonts
Handling errors and edge cases
Migrating from Pages Router to App Router
Skill Structure
.github/skills/next-best-practices/
├── SKILL.md # Quick reference
├── file-conventions.md # Project structure
├── data-patterns.md # Data fetching
├── rsc-boundaries.md # Server/client patterns
├── async-patterns.md # Next.js 15/16 async APIs
├── error-handling.md # Error boundaries
├── image.md # Image optimization
├── font.md # Font optimization
└── metadata.md # SEO & Open Graph
References