Next.js Best Practices
Principles for Next.js App Router development.
1. Server vs Client Components
Decision Tree
Does it need...?
│
├── useState, useEffect, event handlers
│ └── Client Component ('use client')
│
├── Direct data fetching, no interactivity
│ └── Server Component (default)
│
└── Both?
└── Split: Server parent + Client child
By Default
| Type |
Use |
| Server |
Data fetching, layout, static content |
| Client |
Forms, buttons, interactive UI |
2. Data Fetching Patterns
Fetch Strategy
| Pattern |
Use |
| Default |
Static (cached at build) |
| Revalidate |
ISR (time-based refresh) |
| No-store |
Dynamic (every request) |
Data Flow
| Source |
Pattern |
| Database |
Server Component fetch |
| API |
fetch with caching |
| User input |
Client state + server action |
3. Routing Principles
File Conventions
| File |
Purpose |
page.tsx |
Route UI |
layout.tsx |
Shared layout |
loading.tsx |
Loading state |
error.tsx |
Error boundary |
not-found.tsx |
404 page |
Route Organization
| Pattern |
Use |
Route groups (name) |
Organize without URL |
Parallel routes @slot |
Multiple same-level pages |
Intercepting (.) |
Modal overlays |
4. API Routes
Route Handlers
| Method |
Use |
| GET |
Read data |
| POST |
Create data |
| PUT/PATCH |
Update data |
| DELETE |
Remove data |
Best Practices
- Validate input with Zod
- Return proper status codes
- Handle errors gracefully
- Use Edge runtime when possible
5. Performance Principles
Image Optimization
- Use next/image component
- Set priority for above-fold
- Provide blur placeholder
- Use responsive sizes
Bundle Optimization
- Dynamic imports for heavy components
- Route-based code splitting (automatic)
- Analyze with bundle analyzer
6. Metadata
Static vs Dynamic
| Type |
Use |
| Static export |
Fixed metadata |
| generateMetadata |
Dynamic per-route |
Essential Tags
- title (50-60 chars)
- description (150-160 chars)
- Open Graph images
- Canonical URL
7. Caching Strategy
Cache Layers
| Layer |
Control |
| Request |
fetch options |
| Data |
revalidate/tags |
| Full route |
route config |
Revalidation
| Method |
Use |
| Time-based |
revalidate: 60 |
| On-demand |
revalidatePath/Tag |
| No cache |
no-store |
8. Server Actions
Use Cases
- Form submissions
- Data mutations
- Revalidation triggers
Best Practices
- Mark with 'use server'
- Validate all inputs
- Return typed responses
- Handle errors
9. Middleware & Security (Expert)
Middleware Chaining
Don't put everything in one file.
- Login Logic ->
middleware/auth.ts
- Geo Logic ->
middleware/geo.ts
- Main
middleware.ts combines them.
Security Headers
Must Have in next.config.js:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY (Prevent clickjacking)
Content-Security-Policy (Strict CSP prevents XSS)
10. Advanced Caching Rules
Tag-Based Revalidation
Instead of revalidating paths, rely on tags.
- Fetch:
fetch(url, { next: { tags: ['products'] } })
- Action:
revalidateTag('products')
Result: Updates ALL product pages (list, detail, featured) at once.
"Stale-While-Revalidate"
Next.js does this by default for static pages.
- User 1 visits: Sees cached version (FAST). Background background revalidation starts.
- User 2 visits: Sees NEW version.
11. Testing Server Components
Mocking the Unmockable
Since RSCs access DB directly, testing is hard.
Integration Test (Best): Spin up DB, render component, check HTML.
Unit Test (Mocking):
jest.mock('next/headers', () => ({
cookies: () => ({ get: () => ({ value: 'token' }) })
}));
12. Anti-Patterns
| ❌ Don't |
✅ Do |
| 'use client' everywhere |
Server by default |
| Fetch in client components |
Fetch in server |
| Skip loading states |
Use loading.tsx |
| Ignore error boundaries |
Use error.tsx |
| Large client bundles |
Dynamic imports |
10. Project Structure
app/
├── (marketing)/ # Route group
│ └── page.tsx
├── (dashboard)/
│ ├── layout.tsx # Dashboard layout
│ └── page.tsx
├── api/
│ └── [resource]/
│ └── route.ts
└── components/
└── ui/
Remember: Server Components are the default for a reason. Start there, add client only when needed.