
Nextjs Development
- 335 installs
- 61 repo stars
- Updated June 13, 2026
- manutej/luxor-claude-marketplace
Build Next.js App Router pages, layouts, server components, data fetching, metadata, and API routes following framework conventions and performance best practices.
About
Covers Next.js development with App Router conventions, React Server Components, layouts, server actions, async data fetching, metadata, image and font optimization, error boundaries, and API route patterns for production SaaS and content sites.
- App Router file conventions and layouts
- RSC vs client component boundaries
- Server actions and async data patterns
- Metadata, images, and font optimization
- Error handling and route handler APIs
Nextjs Development by the numbers
- 335 all-time installs (skills.sh)
- +20 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #712 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/manutej/luxor-claude-marketplace --skill nextjs-developmentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 335 |
|---|---|
| repo stars | ★ 61 |
| Last updated | June 13, 2026 |
| Repository | manutej/luxor-claude-marketplace ↗ |
What it does
Build Next.js App Router pages, layouts, server components, data fetching, metadata, and API routes following framework conventions and performance best practices.
Files
Next.js Development Skill
This skill provides comprehensive guidance for building modern Next.js applications using the App Router, Server Components, data fetching patterns, routing, API routes, middleware, and full-stack development techniques based on official Next.js documentation.
When to Use This Skill
Use this skill when:
- Building full-stack React applications with server-side rendering (SSR)
- Creating static sites with incremental static regeneration (ISR)
- Developing modern web applications with React Server Components
- Building API backends with serverless route handlers
- Implementing SEO-optimized applications with metadata and Open Graph
- Creating production-ready web applications with built-in optimization
- Building e-commerce, blogs, dashboards, or content-driven sites
- Implementing authentication, data fetching, and complex routing patterns
- Optimizing images, fonts, and performance automatically
- Deploying serverless applications with edge computing capabilities
Core Concepts
App Router
The App Router is Next.js's modern routing system built on React Server Components. It uses the app directory for file-based routing with enhanced features.
Basic App Structure:
app/
├── layout.tsx # Root layout (required)
├── page.tsx # Home page
├── loading.tsx # Loading UI
├── error.tsx # Error UI
├── not-found.tsx # 404 page
├── about/
│ └── page.tsx # /about route
└── blog/
├── page.tsx # /blog route
├── [slug]/
│ └── page.tsx # /blog/[slug] dynamic route
└── layout.tsx # Blog layoutRoot Layout (Required):
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}Page Component:
// app/page.tsx
export default function HomePage() {
return (
<main>
<h1>Welcome to Next.js</h1>
<p>Building modern web applications</p>
</main>
)
}Server Components
Server Components are React components that render on the server. They are the default in the App Router and provide better performance.
Server Component (Default):
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
cache: 'force-cache' // Static generation
})
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<div>
<h1>Blog Posts</h1>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
)
}Client Component (When Needed):
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
)
}Mixing Server and Client Components:
// app/dashboard/page.tsx (Server Component)
import ClientCounter from './ClientCounter'
async function getData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
export default async function DashboardPage() {
const data = await getData()
return (
<div>
<h1>Dashboard</h1>
<p>Server data: {data.value}</p>
{/* Client component for interactivity */}
<ClientCounter />
</div>
)
}Data Fetching
Next.js extends the native fetch() API with automatic caching and revalidation.
Static Data Fetching (Default):
async function getStaticData() {
const res = await fetch('https://api.example.com/data', {
cache: 'force-cache' // Default, equivalent to getStaticProps
})
return res.json()
}
export default async function Page() {
const data = await getStaticData()
return <div>{data.title}</div>
}Dynamic Data Fetching:
async function getDynamicData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store' // Equivalent to getServerSideProps
})
return res.json()
}
export default async function Page() {
const data = await getDynamicData()
return <div>{data.title}</div>
}Revalidation (ISR):
async function getRevalidatedData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 60 } // Revalidate every 60 seconds
})
return res.json()
}
export default async function Page() {
const data = await getRevalidatedData()
return <div>{data.title}</div>
}Parallel Data Fetching:
async function getUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`)
return res.json()
}
async function getUserPosts(id: string) {
const res = await fetch(`https://api.example.com/users/${id}/posts`)
return res.json()
}
export default async function UserPage({ params }: { params: { id: string } }) {
// Fetch in parallel
const [user, posts] = await Promise.all([
getUser(params.id),
getUserPosts(params.id)
])
return (
<div>
<h1>{user.name}</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)
}Sequential Data Fetching:
async function getUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`)
return res.json()
}
async function getRecommendations(preferences: string[]) {
const res = await fetch('https://api.example.com/recommendations', {
method: 'POST',
body: JSON.stringify({ preferences })
})
return res.json()
}
export default async function UserPage({ params }: { params: { id: string } }) {
// First fetch user
const user = await getUser(params.id)
// Then fetch recommendations based on user data
const recommendations = await getRecommendations(user.preferences)
return (
<div>
<h1>{user.name}</h1>
<h2>Recommendations</h2>
<ul>
{recommendations.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
)
}Routing
Next.js uses file-system based routing in the app directory.
Dynamic Routes:
// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
return <h1>Post: {params.slug}</h1>
}
// Generates static pages for these slugs at build time
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return posts.map((post) => ({
slug: post.slug,
}))
}Catch-All Routes:
// app/docs/[...slug]/page.tsx
export default function DocsPage({ params }: { params: { slug: string[] } }) {
// /docs/a/b/c -> params.slug = ['a', 'b', 'c']
return <h1>Docs: {params.slug.join('/')}</h1>
}Optional Catch-All Routes:
// app/shop/[[...slug]]/page.tsx
export default function ShopPage({ params }: { params: { slug?: string[] } }) {
// /shop -> params.slug = undefined
// /shop/clothes -> params.slug = ['clothes']
// /shop/clothes/tops -> params.slug = ['clothes', 'tops']
return <h1>Shop: {params.slug?.join('/') || 'All'}</h1>
}Route Groups:
app/
├── (marketing)/ # Route group (not in URL)
│ ├── about/
│ │ └── page.tsx # /about
│ └── contact/
│ └── page.tsx # /contact
└── (shop)/
├── products/
│ └── page.tsx # /products
└── cart/
└── page.tsx # /cartParallel Routes:
app/
├── @analytics/
│ └── page.tsx
├── @team/
│ └── page.tsx
└── layout.tsx
// app/layout.tsx
export default function Layout({
children,
analytics,
team,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<>
{children}
{analytics}
{team}
</>
)
}Intercepting Routes:
app/
├── feed/
│ └── page.tsx
├── photo/
│ └── [id]/
│ └── page.tsx
└── @modal/
└── (.)photo/
└── [id]/
└── page.tsx # Intercepts /photo/[id] when navigating from /feedLayouts
Layouts wrap pages and preserve state across navigation.
Nested Layouts:
// app/layout.tsx (Root Layout)
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<header>
<nav>Global Navigation</nav>
</header>
{children}
<footer>Global Footer</footer>
</body>
</html>
)
}
// app/dashboard/layout.tsx (Dashboard Layout)
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="dashboard">
<aside>Dashboard Sidebar</aside>
<main>{children}</main>
</div>
)
}
// app/dashboard/page.tsx
export default function DashboardPage() {
return <h1>Dashboard</h1>
}Templates (Re-render on Navigation):
// app/template.tsx
export default function Template({ children }: { children: React.ReactNode }) {
return (
<div>
{/* This creates a new instance on each navigation */}
{children}
</div>
)
}Loading UI
Special loading.tsx files create loading states with React Suspense.
Loading State:
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="spinner">
<p>Loading dashboard...</p>
</div>
)
}
// app/dashboard/page.tsx
async function getData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
export default async function DashboardPage() {
const data = await getData()
return <div>{data.content}</div>
}Streaming with Suspense:
// app/page.tsx
import { Suspense } from 'react'
async function SlowComponent() {
await new Promise(resolve => setTimeout(resolve, 3000))
return <div>Slow data loaded</div>
}
export default function Page() {
return (
<div>
<h1>Page</h1>
<Suspense fallback={<div>Loading slow component...</div>}>
<SlowComponent />
</Suspense>
</div>
)
}Error Handling
Special error.tsx files handle errors with error boundaries.
Error Boundary:
// app/error.tsx
'use client' // Error components must be Client Components
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
)
}Global Error:
// app/global-error.tsx
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<h2>Application Error</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
)
}Not Found:
// app/not-found.tsx
import Link from 'next/link'
export default function NotFound() {
return (
<div>
<h2>Page Not Found</h2>
<p>Could not find the requested resource</p>
<Link href="/">Return Home</Link>
</div>
)
}API Routes
Route Handlers allow you to create API endpoints using Web Request and Response APIs.
Basic API Route:
// app/api/hello/route.ts
export async function GET(request: Request) {
return Response.json({ message: 'Hello from Next.js!' })
}Dynamic Route Handler:
// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const id = params.id
const post = await db.post.findUnique({ where: { id } })
if (!post) {
return new Response('Post not found', { status: 404 })
}
return Response.json(post)
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
await db.post.delete({ where: { id: params.id } })
return new Response(null, { status: 204 })
}POST Request with Body:
// app/api/posts/route.ts
export async function POST(request: Request) {
const body = await request.json()
const post = await db.post.create({
data: {
title: body.title,
content: body.content,
}
})
return Response.json(post, { status: 201 })
}Request with Headers:
// app/api/protected/route.ts
export async function GET(request: Request) {
const token = request.headers.get('authorization')
if (!token) {
return new Response('Unauthorized', { status: 401 })
}
const user = await verifyToken(token)
return Response.json({ user })
}Search Params:
// app/api/search/route.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const query = searchParams.get('q')
const page = searchParams.get('page') || '1'
const results = await search(query, parseInt(page))
return Response.json(results)
}CORS Headers:
// app/api/public/route.ts
export async function GET(request: Request) {
const data = { message: 'Public API' }
return Response.json(data, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}
export async function OPTIONS(request: Request) {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}Middleware
Middleware runs before a request is completed, allowing you to modify the response.
Basic Middleware:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Clone the request headers
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-custom-header', 'custom-value')
// Return response with modified headers
return NextResponse.next({
request: {
headers: requestHeaders,
},
})
}
export const config = {
matcher: '/api/:path*',
}Authentication Middleware:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value
// Redirect to login if no token
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Redirect to dashboard if already logged in
if (token && request.nextUrl.pathname === '/login') {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/login'],
}Geolocation and Rewrites:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const country = request.geo?.country || 'US'
// Rewrite based on country
if (country === 'GB') {
return NextResponse.rewrite(new URL('/gb' + request.nextUrl.pathname, request.url))
}
return NextResponse.next()
}Rate Limiting:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'),
})
export async function middleware(request: NextRequest) {
const ip = request.ip ?? '127.0.0.1'
const { success } = await ratelimit.limit(ip)
if (!success) {
return new Response('Too Many Requests', { status: 429 })
}
return NextResponse.next()
}
export const config = {
matcher: '/api/:path*',
}Metadata and SEO
Next.js provides a Metadata API for defining page metadata.
Static Metadata:
// app/about/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'About Us',
description: 'Learn more about our company',
openGraph: {
title: 'About Us',
description: 'Learn more about our company',
images: ['/og-image.jpg'],
},
twitter: {
card: 'summary_large_image',
},
}
export default function AboutPage() {
return <h1>About Us</h1>
}Dynamic Metadata:
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
type Props = {
params: { slug: string }
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
type: 'article',
publishedTime: post.publishedAt,
},
}
}
export default async function BlogPost({ params }: Props) {
const post = await getPost(params.slug)
return <article>{post.content}</article>
}Metadata with Icons:
// app/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: {
default: 'My App',
template: '%s | My App',
},
description: 'My application description',
icons: {
icon: '/favicon.ico',
apple: '/apple-icon.png',
},
manifest: '/manifest.json',
}Image Optimization
Next.js automatically optimizes images with the Image component.
Basic Image:
import Image from 'next/image'
export default function Page() {
return (
<Image
src="/profile.jpg"
alt="Profile picture"
width={500}
height={500}
/>
)
}Remote Images:
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
],
},
}
// Component
import Image from 'next/image'
export default function Page() {
return (
<Image
src="https://images.unsplash.com/photo-1234567890"
alt="Photo"
width={800}
height={600}
priority // Load image with high priority
/>
)
}Fill Container:
import Image from 'next/image'
export default function Page() {
return (
<div style={{ position: 'relative', width: '100%', height: '400px' }}>
<Image
src="/background.jpg"
alt="Background"
fill
style={{ objectFit: 'cover' }}
/>
</div>
)
}Responsive Images:
import Image from 'next/image'
export default function Page() {
return (
<Image
src="/hero.jpg"
alt="Hero"
width={1920}
height={1080}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
)
}Font Optimization
Next.js automatically optimizes fonts with next/font.
Google Fonts:
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
})
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono',
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.className} ${robotoMono.variable}`}>
<body>{children}</body>
</html>
)
}Local Fonts:
// app/layout.tsx
import localFont from 'next/font/local'
const myFont = localFont({
src: './fonts/my-font.woff2',
display: 'swap',
variable: '--font-my-font',
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={myFont.variable}>
<body>{children}</body>
</html>
)
}Workflow Patterns
Creating a New Page
1. Create a new folder in app directory 2. Add a page.tsx file 3. Export a default component 4. Optionally add layout, loading, and error files
// app/products/page.tsx
export default function ProductsPage() {
return <h1>Products</h1>
}
// app/products/layout.tsx
export default function ProductsLayout({ children }: { children: React.ReactNode }) {
return (
<div>
<nav>Products Navigation</nav>
{children}
</div>
)
}
// app/products/loading.tsx
export default function Loading() {
return <div>Loading products...</div>
}Server Actions
Server Actions allow you to run server-side code directly from client components.
Form with Server Action:
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
await db.post.create({
data: { title, content }
})
revalidatePath('/posts')
}
// app/new-post/page.tsx
import { createPost } from '../actions'
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="Title" required />
<textarea name="content" placeholder="Content" required />
<button type="submit">Create Post</button>
</form>
)
}Server Action with useTransition:
// app/actions.ts
'use server'
export async function updateUser(userId: string, data: UserData) {
await db.user.update({
where: { id: userId },
data,
})
return { success: true }
}
// app/profile/page.tsx
'use client'
import { useTransition } from 'react'
import { updateUser } from '../actions'
export default function ProfilePage() {
const [isPending, startTransition] = useTransition()
const handleSubmit = (formData: FormData) => {
startTransition(async () => {
await updateUser('user-id', {
name: formData.get('name') as string,
})
})
}
return (
<form action={handleSubmit}>
<input name="name" />
<button disabled={isPending}>
{isPending ? 'Saving...' : 'Save'}
</button>
</form>
)
}Authentication Pattern
Basic Authentication with Middleware:
// lib/auth.ts
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
export async function getSession() {
const session = cookies().get('session')?.value
if (!session) return null
return await verifySession(session)
}
export async function requireAuth() {
const session = await getSession()
if (!session) {
redirect('/login')
}
return session
}
// app/dashboard/page.tsx
import { requireAuth } from '@/lib/auth'
export default async function DashboardPage() {
const session = await requireAuth()
return (
<div>
<h1>Welcome, {session.user.name}</h1>
</div>
)
}
// app/api/login/route.ts
import { cookies } from 'next/headers'
export async function POST(request: Request) {
const { email, password } = await request.json()
const user = await validateCredentials(email, password)
if (!user) {
return Response.json({ error: 'Invalid credentials' }, { status: 401 })
}
const session = await createSession(user)
cookies().set('session', session, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7, // 1 week
})
return Response.json({ success: true })
}Database Pattern
Prisma with Server Components:
// lib/db.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = global as unknown as { prisma: PrismaClient }
export const prisma = globalForPrisma.prisma || new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
// app/posts/page.tsx
import { prisma } from '@/lib/db'
async function getPosts() {
return await prisma.post.findMany({
orderBy: { createdAt: 'desc' },
include: { author: true },
})
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<div>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>By {post.author.name}</p>
<p>{post.content}</p>
</article>
))}
</div>
)
}Caching Strategies
Revalidate on Demand:
// app/actions.ts
'use server'
import { revalidatePath, revalidateTag } from 'next/cache'
export async function createPost(data: PostData) {
await db.post.create({ data })
// Revalidate specific path
revalidatePath('/posts')
// Or revalidate by tag
revalidateTag('posts')
}
// Fetch with cache tags
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }
})
return res.json()
}Time-based Revalidation:
async function getData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // Revalidate every hour
})
return res.json()
}Route Segment Config:
// app/blog/page.tsx
export const revalidate = 3600 // Revalidate every hour
export const dynamic = 'force-static' // Force static generation
export const fetchCache = 'force-cache' // Force cache for all fetch requests
export default async function BlogPage() {
const posts = await getPosts()
return <div>{/* ... */}</div>
}Best Practices
1. Use Server Components by Default
Server Components are the default and provide better performance. Only use Client Components when needed.
// ✅ Good - Server Component
async function getData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
export default async function Page() {
const data = await getData()
return <div>{data.title}</div>
}
// ✅ Good - Client Component only when needed
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}2. Fetch Data Where You Need It
Don't prop-drill data. Fetch data in the component that needs it.
// ✅ Good - Fetch where needed
async function Header() {
const user = await getUser()
return <div>Welcome, {user.name}</div>
}
async function Posts() {
const posts = await getPosts()
return <div>{/* render posts */}</div>
}
export default function Page() {
return (
<>
<Header />
<Posts />
</>
)
}3. Use Parallel Data Fetching
Fetch data in parallel when possible to reduce loading time.
// ✅ Good - Parallel fetching
export default async function Page() {
const [user, posts] = await Promise.all([
getUser(),
getPosts()
])
return <div>{/* ... */}</div>
}4. Optimize Images
Always use the Image component for automatic optimization.
// ✅ Good
import Image from 'next/image'
<Image
src="/hero.jpg"
alt="Hero"
width={1920}
height={1080}
priority
/>
// ❌ Bad
<img src="/hero.jpg" alt="Hero" />5. Use Metadata API for SEO
Define metadata for every page.
// ✅ Good
export const metadata = {
title: 'My Page',
description: 'Page description',
}
export default function Page() {
return <div>Content</div>
}6. Implement Error Boundaries
Add error.tsx files for error handling.
// app/error.tsx
'use client'
export default function Error({ error, reset }: { error: Error, reset: () => void }) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
)
}7. Use Loading States
Add loading.tsx files for loading UI.
// app/loading.tsx
export default function Loading() {
return <div>Loading...</div>
}8. Implement Route Handlers for APIs
Use Route Handlers instead of API routes in pages directory.
// ✅ Good - app/api/users/route.ts
export async function GET(request: Request) {
const users = await db.user.findMany()
return Response.json(users)
}9. Use Middleware for Common Logic
Implement authentication, redirects, and rewrites in middleware.
// middleware.ts
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}10. Optimize Fonts
Use next/font for automatic font optimization.
// ✅ Good
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export default function RootLayout({ children }) {
return (
<html className={inter.className}>
<body>{children}</body>
</html>
)
}Common Patterns
Blog Pattern
// app/blog/page.tsx
import Link from 'next/link'
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }
})
return res.json()
}
export default async function BlogPage() {
const posts = await getPosts()
return (
<div>
<h1>Blog</h1>
{posts.map((post) => (
<article key={post.id}>
<h2>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
)
}
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`)
if (!res.ok) return null
return res.json()
}
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return posts.map((post) => ({ slug: post.slug }))
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
if (!post) return {}
return {
title: post.title,
description: post.excerpt,
}
}
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
if (!post) {
notFound()
}
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}E-commerce Pattern
// app/products/page.tsx
import { Suspense } from 'react'
import ProductGrid from './ProductGrid'
import Filters from './Filters'
export default function ProductsPage({
searchParams,
}: {
searchParams: { category?: string; sort?: string }
}) {
return (
<div>
<h1>Products</h1>
<Filters />
<Suspense fallback={<div>Loading products...</div>}>
<ProductGrid searchParams={searchParams} />
</Suspense>
</div>
)
}
// app/products/ProductGrid.tsx
async function getProducts(category?: string, sort?: string) {
const params = new URLSearchParams()
if (category) params.set('category', category)
if (sort) params.set('sort', sort)
const res = await fetch(`https://api.example.com/products?${params}`, {
next: { revalidate: 300 }
})
return res.json()
}
export default async function ProductGrid({
searchParams,
}: {
searchParams: { category?: string; sort?: string }
}) {
const products = await getProducts(searchParams.category, searchParams.sort)
return (
<div className="grid">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}
// app/products/[id]/page.tsx
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`)
return res.json()
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id)
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>${product.price}</p>
<AddToCartButton productId={product.id} />
</div>
)
}Dashboard Pattern
// app/dashboard/layout.tsx
import { requireAuth } from '@/lib/auth'
import Sidebar from './Sidebar'
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const session = await requireAuth()
return (
<div className="dashboard">
<Sidebar user={session.user} />
<main>{children}</main>
</div>
)
}
// app/dashboard/page.tsx
import { Suspense } from 'react'
import Stats from './Stats'
import RecentActivity from './RecentActivity'
import Chart from './Chart'
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading stats...</div>}>
<Stats />
</Suspense>
<div className="grid">
<Suspense fallback={<div>Loading chart...</div>}>
<Chart />
</Suspense>
<Suspense fallback={<div>Loading activity...</div>}>
<RecentActivity />
</Suspense>
</div>
</div>
)
}Summary
This Next.js development skill covers:
1. App Router: Modern routing with file-based system 2. Server Components: Default server-side rendering for better performance 3. Data Fetching: Extended fetch API with caching and revalidation 4. Routing: Dynamic routes, catch-all routes, route groups, parallel routes 5. Layouts: Nested layouts, templates, and layout composition 6. Loading States: Automatic loading UI with Suspense 7. Error Handling: Error boundaries and not-found pages 8. API Routes: Route handlers with Web APIs 9. Middleware: Request interception and modification 10. Metadata: SEO optimization with Metadata API 11. Image Optimization: Automatic image optimization with Image component 12. Font Optimization: Automatic font loading with next/font 13. Server Actions: Server-side mutations from client components 14. Authentication: Session management and protected routes 15. Best Practices: Performance optimization, caching strategies, and common patterns
All patterns are based on official Next.js documentation (Trust Score: 10) and represent modern Next.js 13+ App Router development practices.
Next.js Development Examples
Comprehensive code examples demonstrating Next.js patterns and best practices.
Table of Contents
1. Basic App Structure 2. Dynamic Routes with Static Generation 3. Server Components with Data Fetching 4. Client Components with Interactivity 5. Parallel Data Fetching 6. API Route Handlers 7. Form Handling with Server Actions 8. Authentication with Middleware 9. Error Handling and Loading States 10. Streaming with Suspense 11. Dynamic Metadata and SEO 12. Image Optimization 13. Middleware for Rate Limiting 14. E-commerce Product Catalog 15. Blog with MDX Content 16. Dashboard with Protected Routes 17. Search with Server Actions 18. Internationalization (i18n) 19. Real-time Updates with Server-Sent Events 20. Advanced Caching Strategies
---
1. Basic App Structure
A complete basic Next.js application structure with all essential files.
// app/layout.tsx
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'My Next.js App',
description: 'A modern Next.js application',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<nav className="navbar">
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/blog">Blog</a>
</nav>
<main>{children}</main>
<footer>© 2024 My App</footer>
</body>
</html>
)
}
// app/page.tsx
export default function HomePage() {
return (
<div>
<h1>Welcome to Next.js</h1>
<p>Start building your application with the App Router.</p>
</div>
)
}
// app/about/page.tsx
export const metadata = {
title: 'About Us',
description: 'Learn more about our company',
}
export default function AboutPage() {
return (
<div>
<h1>About Us</h1>
<p>We build amazing web applications with Next.js.</p>
</div>
)
}
// app/loading.tsx
export default function Loading() {
return (
<div className="loading-spinner">
<div className="spinner"></div>
<p>Loading...</p>
</div>
)
}
// app/not-found.tsx
import Link from 'next/link'
export default function NotFound() {
return (
<div>
<h2>404 - Page Not Found</h2>
<p>The page you're looking for doesn't exist.</p>
<Link href="/">Go back home</Link>
</div>
)
}---
2. Dynamic Routes with Static Generation
Building a blog with dynamic routes and static page generation.
// app/blog/page.tsx
import Link from 'next/link'
interface Post {
id: string
slug: string
title: string
excerpt: string
publishedAt: string
}
async function getPosts(): Promise<Post[]> {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // Revalidate every hour
})
if (!res.ok) {
throw new Error('Failed to fetch posts')
}
return res.json()
}
export default async function BlogPage() {
const posts = await getPosts()
return (
<div className="blog-container">
<h1>Blog</h1>
<div className="posts-grid">
{posts.map((post) => (
<article key={post.id} className="post-card">
<h2>
<Link href={`/blog/${post.slug}`}>
{post.title}
</Link>
</h2>
<p>{post.excerpt}</p>
<time>{new Date(post.publishedAt).toLocaleDateString()}</time>
</article>
))}
</div>
</div>
)
}
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
import type { Metadata } from 'next'
interface Post {
id: string
slug: string
title: string
content: string
excerpt: string
publishedAt: string
author: {
name: string
avatar: string
}
}
async function getPost(slug: string): Promise<Post | null> {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 3600 }
})
if (!res.ok) {
return null
}
return res.json()
}
// Generate static paths at build time
export async function generateStaticParams() {
const res = await fetch('https://api.example.com/posts')
const posts: Post[] = await res.json()
return posts.map((post) => ({
slug: post.slug,
}))
}
// Generate dynamic metadata
export async function generateMetadata({
params,
}: {
params: { slug: string }
}): Promise<Metadata> {
const post = await getPost(params.slug)
if (!post) {
return {
title: 'Post Not Found',
}
}
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: 'article',
publishedTime: post.publishedAt,
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
},
}
}
export default async function BlogPostPage({
params,
}: {
params: { slug: string }
}) {
const post = await getPost(params.slug)
if (!post) {
notFound()
}
return (
<article className="blog-post">
<header>
<h1>{post.title}</h1>
<div className="author-info">
<img src={post.author.avatar} alt={post.author.name} />
<div>
<p>{post.author.name}</p>
<time>{new Date(post.publishedAt).toLocaleDateString()}</time>
</div>
</div>
</header>
<div
className="content"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
</article>
)
}---
3. Server Components with Data Fetching
Demonstrating various data fetching patterns in Server Components.
// app/dashboard/page.tsx
import { Suspense } from 'react'
// Static data - cached indefinitely
async function getAppConfig() {
const res = await fetch('https://api.example.com/config', {
cache: 'force-cache'
})
return res.json()
}
// Dynamic data - no caching
async function getUserData() {
const res = await fetch('https://api.example.com/user', {
cache: 'no-store'
})
return res.json()
}
// Revalidated data - cached with time-based revalidation
async function getStats() {
const res = await fetch('https://api.example.com/stats', {
next: { revalidate: 60 } // Revalidate every 60 seconds
})
return res.json()
}
export default async function DashboardPage() {
// Parallel fetching
const [config, user] = await Promise.all([
getAppConfig(),
getUserData()
])
return (
<div className="dashboard">
<h1>Welcome, {user.name}</h1>
<div className="stats-section">
<Suspense fallback={<StatsLoading />}>
<Stats />
</Suspense>
</div>
<div className="config-section">
<h2>Settings</h2>
<pre>{JSON.stringify(config, null, 2)}</pre>
</div>
</div>
)
}
async function Stats() {
const stats = await getStats()
return (
<div className="stats-grid">
<div className="stat-card">
<h3>Total Users</h3>
<p>{stats.totalUsers}</p>
</div>
<div className="stat-card">
<h3>Active Sessions</h3>
<p>{stats.activeSessions}</p>
</div>
<div className="stat-card">
<h3>Revenue</h3>
<p>${stats.revenue}</p>
</div>
</div>
)
}
function StatsLoading() {
return (
<div className="stats-grid">
{[1, 2, 3].map((i) => (
<div key={i} className="stat-card skeleton">
<div className="skeleton-title"></div>
<div className="skeleton-value"></div>
</div>
))}
</div>
)
}---
4. Client Components with Interactivity
Creating interactive components that run on the client.
// app/components/Counter.tsx
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div className="counter">
<p>Count: {count}</p>
<div className="button-group">
<button onClick={() => setCount(count - 1)}>-</button>
<button onClick={() => setCount(0)}>Reset</button>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
</div>
)
}
// app/components/SearchBar.tsx
'use client'
import { useState, useEffect } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
export default function SearchBar() {
const router = useRouter()
const searchParams = useSearchParams()
const [query, setQuery] = useState(searchParams.get('q') || '')
useEffect(() => {
// Debounce search
const timer = setTimeout(() => {
if (query) {
router.push(`/search?q=${encodeURIComponent(query)}`)
}
}, 500)
return () => clearTimeout(timer)
}, [query, router])
return (
<div className="search-bar">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
</div>
)
}
// app/components/Theme Provider.tsx
'use client'
import { createContext, useContext, useState, useEffect } from 'react'
type Theme = 'light' | 'dark'
const ThemeContext = createContext<{
theme: Theme
toggleTheme: () => void
}>({
theme: 'light',
toggleTheme: () => {},
})
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>('light')
useEffect(() => {
// Load theme from localStorage
const savedTheme = localStorage.getItem('theme') as Theme
if (savedTheme) {
setTheme(savedTheme)
}
}, [])
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light'
setTheme(newTheme)
localStorage.setItem('theme', newTheme)
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<div data-theme={theme}>{children}</div>
</ThemeContext.Provider>
)
}
export function useTheme() {
return useContext(ThemeContext)
}
// app/components/ThemeToggle.tsx
'use client'
import { useTheme } from './ThemeProvider'
export default function ThemeToggle() {
const { theme, toggleTheme } = useTheme()
return (
<button onClick={toggleTheme} className="theme-toggle">
{theme === 'light' ? '🌙' : '☀️'}
</button>
)
}---
5. Parallel Data Fetching
Optimizing performance by fetching data in parallel.
// app/user/[id]/page.tsx
interface User {
id: string
name: string
email: string
bio: string
}
interface Post {
id: string
title: string
excerpt: string
}
interface Activity {
id: string
type: string
description: string
timestamp: string
}
async function getUser(id: string): Promise<User> {
const res = await fetch(`https://api.example.com/users/${id}`)
return res.json()
}
async function getUserPosts(id: string): Promise<Post[]> {
const res = await fetch(`https://api.example.com/users/${id}/posts`)
return res.json()
}
async function getUserActivity(id: string): Promise<Activity[]> {
const res = await fetch(`https://api.example.com/users/${id}/activity`)
return res.json()
}
export default async function UserProfilePage({
params,
}: {
params: { id: string }
}) {
// Fetch all data in parallel
const [user, posts, activity] = await Promise.all([
getUser(params.id),
getUserPosts(params.id),
getUserActivity(params.id),
])
return (
<div className="user-profile">
<header className="profile-header">
<h1>{user.name}</h1>
<p>{user.email}</p>
<p>{user.bio}</p>
</header>
<div className="profile-content">
<section className="posts-section">
<h2>Recent Posts</h2>
{posts.map((post) => (
<article key={post.id} className="post-preview">
<h3>{post.title}</h3>
<p>{post.excerpt}</p>
</article>
))}
</section>
<aside className="activity-sidebar">
<h2>Recent Activity</h2>
{activity.map((item) => (
<div key={item.id} className="activity-item">
<p>{item.description}</p>
<time>{new Date(item.timestamp).toLocaleString()}</time>
</div>
))}
</aside>
</div>
</div>
)
}
// Example with waterfall pattern (sequential)
async function getRecommendations(userId: string, preferences: string[]) {
const res = await fetch('https://api.example.com/recommendations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, preferences }),
})
return res.json()
}
export async function UserWithRecommendations({
params,
}: {
params: { id: string }
}) {
// First, get user data
const user = await getUser(params.id)
// Then, get recommendations based on user preferences
const recommendations = await getRecommendations(
params.id,
user.preferences
)
return (
<div>
<h1>{user.name}</h1>
<div className="recommendations">
<h2>Recommended for You</h2>
{recommendations.map((item) => (
<div key={item.id}>{item.title}</div>
))}
</div>
</div>
)
}---
6. API Route Handlers
Building RESTful API endpoints with Route Handlers.
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
// Validation schema
const postSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
published: z.boolean().optional(),
})
// GET /api/posts
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '10')
const search = searchParams.get('search') || ''
try {
const posts = await db.post.findMany({
where: search
? {
OR: [
{ title: { contains: search, mode: 'insensitive' } },
{ content: { contains: search, mode: 'insensitive' } },
],
}
: {},
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
include: { author: true },
})
const total = await db.post.count()
return NextResponse.json({
posts,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
})
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch posts' },
{ status: 500 }
)
}
}
// POST /api/posts
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const validated = postSchema.parse(body)
const post = await db.post.create({
data: {
title: validated.title,
content: validated.content,
published: validated.published ?? false,
authorId: 'current-user-id', // Get from session
},
})
return NextResponse.json(post, { 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/api/posts/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const post = await db.post.findUnique({
where: { id: params.id },
include: { author: true, comments: true },
})
if (!post) {
return NextResponse.json(
{ error: 'Post not found' },
{ status: 404 }
)
}
return NextResponse.json(post)
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch post' },
{ status: 500 }
)
}
}
export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const body = await request.json()
const validated = postSchema.partial().parse(body)
const post = await db.post.update({
where: { id: params.id },
data: validated,
})
return NextResponse.json(post)
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Validation failed', details: error.errors },
{ status: 400 }
)
}
return NextResponse.json(
{ error: 'Failed to update post' },
{ status: 500 }
)
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
await db.post.delete({
where: { id: params.id },
})
return new NextResponse(null, { status: 204 })
} catch (error) {
return NextResponse.json(
{ error: 'Failed to delete post' },
{ status: 500 }
)
}
}
// app/api/upload/route.ts
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const file = formData.get('file') as File
if (!file) {
return NextResponse.json(
{ error: 'No file provided' },
{ status: 400 }
)
}
// Upload to cloud storage (e.g., AWS S3, Cloudinary)
const buffer = Buffer.from(await file.arrayBuffer())
const url = await uploadToStorage(buffer, file.name)
return NextResponse.json({ url })
} catch (error) {
return NextResponse.json(
{ error: 'Failed to upload file' },
{ status: 500 }
)
}
}---
7. Form Handling with Server Actions
Using Server Actions for form submissions and mutations.
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { z } from 'zod'
const postSchema = z.object({
title: z.string().min(1, 'Title is required').max(200),
content: z.string().min(1, 'Content is required'),
published: z.boolean().optional(),
})
export async function createPost(formData: FormData) {
const validated = postSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
published: formData.get('published') === 'on',
})
if (!validated.success) {
return {
error: 'Validation failed',
details: validated.error.flatten().fieldErrors,
}
}
try {
const post = await db.post.create({
data: validated.data,
})
revalidatePath('/posts')
redirect(`/posts/${post.id}`)
} catch (error) {
return { error: 'Failed to create post' }
}
}
export async function updatePost(id: string, formData: FormData) {
const validated = postSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
published: formData.get('published') === 'on',
})
if (!validated.success) {
return {
error: 'Validation failed',
details: validated.error.flatten().fieldErrors,
}
}
try {
await db.post.update({
where: { id },
data: validated.data,
})
revalidatePath(`/posts/${id}`)
revalidatePath('/posts')
return { success: true }
} catch (error) {
return { error: 'Failed to update post' }
}
}
export async function deletePost(id: string) {
try {
await db.post.delete({
where: { id },
})
revalidatePath('/posts')
redirect('/posts')
} catch (error) {
return { error: 'Failed to delete post' }
}
}
// app/posts/new/page.tsx
import { createPost } from '@/app/actions'
import SubmitButton from './SubmitButton'
export default function NewPostPage() {
return (
<div className="new-post-page">
<h1>Create New Post</h1>
<form action={createPost} className="post-form">
<div className="form-group">
<label htmlFor="title">Title</label>
<input
type="text"
id="title"
name="title"
required
placeholder="Enter post title"
/>
</div>
<div className="form-group">
<label htmlFor="content">Content</label>
<textarea
id="content"
name="content"
required
rows={10}
placeholder="Write your post content"
/>
</div>
<div className="form-group">
<label>
<input type="checkbox" name="published" />
Publish immediately
</label>
</div>
<SubmitButton />
</form>
</div>
)
}
// app/posts/new/SubmitButton.tsx
'use client'
import { useFormStatus } from 'react-dom'
export default function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending} className="submit-button">
{pending ? 'Creating...' : 'Create Post'}
</button>
)
}
// app/posts/[id]/edit/page.tsx
import { updatePost, deletePost } from '@/app/actions'
import { notFound } from 'next/navigation'
async function getPost(id: string) {
const post = await db.post.findUnique({
where: { id },
})
if (!post) {
notFound()
}
return post
}
export default async function EditPostPage({
params,
}: {
params: { id: string }
}) {
const post = await getPost(params.id)
const updatePostWithId = updatePost.bind(null, params.id)
const deletePostWithId = deletePost.bind(null, params.id)
return (
<div className="edit-post-page">
<h1>Edit Post</h1>
<form action={updatePostWithId} className="post-form">
<div className="form-group">
<label htmlFor="title">Title</label>
<input
type="text"
id="title"
name="title"
defaultValue={post.title}
required
/>
</div>
<div className="form-group">
<label htmlFor="content">Content</label>
<textarea
id="content"
name="content"
defaultValue={post.content}
required
rows={10}
/>
</div>
<div className="form-group">
<label>
<input
type="checkbox"
name="published"
defaultChecked={post.published}
/>
Published
</label>
</div>
<div className="button-group">
<button type="submit" className="submit-button">
Update Post
</button>
</div>
</form>
<form action={deletePostWithId} className="delete-form">
<button type="submit" className="delete-button">
Delete Post
</button>
</form>
</div>
)
}---
8. Authentication with Middleware
Implementing authentication and protected routes.
// lib/auth.ts
import { cookies } from 'next/headers'
import { jwtVerify, SignJWT } from 'jose'
const secret = new TextEncoder().encode(process.env.JWT_SECRET)
export async function createSession(userId: string) {
const token = await new SignJWT({ userId })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(secret)
return token
}
export async function verifySession(token: string) {
try {
const { payload } = await jwtVerify(token, secret)
return payload
} catch (error) {
return null
}
}
export async function getSession() {
const token = cookies().get('session')?.value
if (!token) {
return null
}
return await verifySession(token)
}
export async function getCurrentUser() {
const session = await getSession()
if (!session) {
return null
}
const user = await db.user.findUnique({
where: { id: session.userId as string },
})
return user
}
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { verifySession } from './lib/auth'
const publicPaths = ['/login', '/register', '/']
const authPaths = ['/login', '/register']
export async function middleware(request: NextRequest) {
const path = request.nextUrl.pathname
const token = request.cookies.get('session')?.value
// Check if path is public
const isPublicPath = publicPaths.some((p) => path.startsWith(p))
const isAuthPath = authPaths.some((p) => path.startsWith(p))
// Verify session
const session = token ? await verifySession(token) : null
// Redirect to login if accessing protected route without session
if (!isPublicPath && !session) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Redirect to dashboard if accessing auth pages with valid session
if (isAuthPath && session) {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}
// app/api/auth/login/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { cookies } from 'next/headers'
import { createSession } from '@/lib/auth'
import bcrypt from 'bcryptjs'
export async function POST(request: NextRequest) {
try {
const { email, password } = await request.json()
// Find user
const user = await db.user.findUnique({
where: { email },
})
if (!user) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
)
}
// Verify password
const isValid = await bcrypt.compare(password, user.password)
if (!isValid) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
)
}
// Create session
const token = await createSession(user.id)
cookies().set('session', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 1 week
})
return NextResponse.json({
user: {
id: user.id,
name: user.name,
email: user.email,
},
})
} catch (error) {
return NextResponse.json(
{ error: 'Authentication failed' },
{ status: 500 }
)
}
}
// app/api/auth/logout/route.ts
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
export async function POST() {
cookies().delete('session')
return NextResponse.json({ success: true })
}
// app/dashboard/page.tsx
import { getCurrentUser } from '@/lib/auth'
import { redirect } from 'next/navigation'
export default async function DashboardPage() {
const user = await getCurrentUser()
if (!user) {
redirect('/login')
}
return (
<div className="dashboard">
<h1>Welcome, {user.name}</h1>
<p>Email: {user.email}</p>
</div>
)
}
// app/login/page.tsx
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
export default function LoginPage() {
const router = useRouter()
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setLoading(true)
setError('')
const formData = new FormData(e.currentTarget)
const email = formData.get('email')
const password = formData.get('password')
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
if (!res.ok) {
const data = await res.json()
setError(data.error || 'Login failed')
return
}
router.push('/dashboard')
router.refresh()
} catch (error) {
setError('An error occurred')
} finally {
setLoading(false)
}
}
return (
<div className="login-page">
<h1>Login</h1>
<form onSubmit={handleSubmit} className="login-form">
{error && <div className="error">{error}</div>}
<div className="form-group">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
required
autoComplete="email"
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
name="password"
required
autoComplete="current-password"
/>
</div>
<button type="submit" disabled={loading}>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
</div>
)
}---
9. Error Handling and Loading States
Implementing comprehensive error handling and loading UI.
// app/error.tsx
'use client'
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// Log error to error reporting service
console.error('Error:', error)
}, [error])
return (
<div className="error-container">
<h2>Something went wrong!</h2>
<p>{error.message}</p>
{error.digest && <p className="error-digest">Error ID: {error.digest}</p>}
<button onClick={reset} className="retry-button">
Try again
</button>
</div>
)
}
// app/global-error.tsx
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<div className="global-error">
<h2>Application Error</h2>
<p>An unexpected error occurred</p>
<button onClick={reset}>Try again</button>
</div>
</body>
</html>
)
}
// app/loading.tsx
export default function Loading() {
return (
<div className="loading-container">
<div className="spinner">
<div className="bounce1"></div>
<div className="bounce2"></div>
<div className="bounce3"></div>
</div>
<p>Loading...</p>
</div>
)
}
// app/posts/[id]/loading.tsx
export default function PostLoading() {
return (
<div className="post-skeleton">
<div className="skeleton skeleton-title"></div>
<div className="skeleton skeleton-meta"></div>
<div className="skeleton skeleton-content"></div>
<div className="skeleton skeleton-content"></div>
<div className="skeleton skeleton-content short"></div>
</div>
)
}
// app/posts/[id]/error.tsx
'use client'
import Link from 'next/link'
export default function PostError({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<div className="post-error">
<h2>Failed to load post</h2>
<p>{error.message}</p>
<div className="error-actions">
<button onClick={reset}>Try again</button>
<Link href="/posts">Back to posts</Link>
</div>
</div>
)
}
// app/posts/[id]/not-found.tsx
import Link from 'next/link'
export default function PostNotFound() {
return (
<div className="not-found">
<h2>Post Not Found</h2>
<p>The post you're looking for doesn't exist.</p>
<Link href="/posts" className="back-link">
View all posts
</Link>
</div>
)
}---
10. Streaming with Suspense
Implementing streaming and progressive rendering.
// app/dashboard/page.tsx
import { Suspense } from 'react'
export default function DashboardPage() {
return (
<div className="dashboard">
<h1>Dashboard</h1>
{/* Immediately render header */}
<header className="dashboard-header">
<WelcomeMessage />
</header>
{/* Stream different sections independently */}
<div className="dashboard-grid">
<Suspense fallback={<CardSkeleton />}>
<UserStats />
</Suspense>
<Suspense fallback={<CardSkeleton />}>
<RecentOrders />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<SalesChart />
</Suspense>
<Suspense fallback={<CardSkeleton />}>
<TopProducts />
</Suspense>
</div>
</div>
)
}
async function UserStats() {
// Simulate slow data fetch
await new Promise((resolve) => setTimeout(resolve, 1000))
const stats = await fetch('https://api.example.com/stats').then((r) =>
r.json()
)
return (
<div className="stats-card">
<h2>Statistics</h2>
<div className="stats-grid">
<div className="stat">
<span className="stat-label">Total Users</span>
<span className="stat-value">{stats.totalUsers}</span>
</div>
<div className="stat">
<span className="stat-label">Active Users</span>
<span className="stat-value">{stats.activeUsers}</span>
</div>
<div className="stat">
<span className="stat-label">New Today</span>
<span className="stat-value">{stats.newToday}</span>
</div>
</div>
</div>
)
}
async function RecentOrders() {
await new Promise((resolve) => setTimeout(resolve, 1500))
const orders = await fetch('https://api.example.com/orders/recent').then(
(r) => r.json()
)
return (
<div className="orders-card">
<h2>Recent Orders</h2>
<ul className="orders-list">
{orders.map((order) => (
<li key={order.id} className="order-item">
<span>{order.customerName}</span>
<span>${order.total}</span>
</li>
))}
</ul>
</div>
)
}
async function SalesChart() {
await new Promise((resolve) => setTimeout(resolve, 2000))
const salesData = await fetch('https://api.example.com/sales/chart').then(
(r) => r.json()
)
return (
<div className="chart-card">
<h2>Sales Overview</h2>
{/* Render chart component */}
<ChartComponent data={salesData} />
</div>
)
}
async function TopProducts() {
await new Promise((resolve) => setTimeout(resolve, 1200))
const products = await fetch('https://api.example.com/products/top').then(
(r) => r.json()
)
return (
<div className="products-card">
<h2>Top Products</h2>
<ul className="products-list">
{products.map((product) => (
<li key={product.id} className="product-item">
<img src={product.image} alt={product.name} />
<div>
<p>{product.name}</p>
<span>{product.sales} sales</span>
</div>
</li>
))}
</ul>
</div>
)
}
function CardSkeleton() {
return (
<div className="card-skeleton">
<div className="skeleton skeleton-title"></div>
<div className="skeleton skeleton-content"></div>
<div className="skeleton skeleton-content"></div>
</div>
)
}
function ChartSkeleton() {
return (
<div className="chart-skeleton">
<div className="skeleton skeleton-title"></div>
<div className="skeleton skeleton-chart"></div>
</div>
)
}
function WelcomeMessage() {
return (
<div className="welcome">
<h2>Welcome back!</h2>
<p>Here's what's happening today</p>
</div>
)
}---
11. Dynamic Metadata and SEO
Advanced SEO optimization with dynamic metadata.
// app/blog/[slug]/page.tsx
import type { Metadata, ResolvingMetadata } from 'next'
import { notFound } from 'next/navigation'
interface Post {
id: string
slug: string
title: string
excerpt: string
content: string
publishedAt: string
updatedAt: string
coverImage: string
author: {
name: string
image: string
}
tags: string[]
}
async function getPost(slug: string): Promise<Post | null> {
const res = await fetch(`https://api.example.com/posts/${slug}`)
if (!res.ok) return null
return res.json()
}
export async function generateMetadata(
{ params }: { params: { slug: string } },
parent: ResolvingMetadata
): Promise<Metadata> {
const post = await getPost(params.slug)
if (!post) {
return {
title: 'Post Not Found',
}
}
const previousImages = (await parent).openGraph?.images || []
return {
title: post.title,
description: post.excerpt,
authors: [{ name: post.author.name }],
openGraph: {
title: post.title,
description: post.excerpt,
type: 'article',
publishedTime: post.publishedAt,
modifiedTime: post.updatedAt,
authors: [post.author.name],
images: [post.coverImage, ...previousImages],
tags: post.tags,
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.coverImage],
creator: '@yourhandle',
},
alternates: {
canonical: `https://yourdomain.com/blog/${post.slug}`,
},
keywords: post.tags,
}
}
export default async function BlogPostPage({
params,
}: {
params: { slug: string }
}) {
const post = await getPost(params.slug)
if (!post) {
notFound()
}
return (
<article className="blog-post" itemScope itemType="https://schema.org/BlogPosting">
<meta itemProp="datePublished" content={post.publishedAt} />
<meta itemProp="dateModified" content={post.updatedAt} />
<header>
<h1 itemProp="headline">{post.title}</h1>
<div className="author-info" itemScope itemType="https://schema.org/Person">
<img
src={post.author.image}
alt={post.author.name}
itemProp="image"
/>
<span itemProp="name">{post.author.name}</span>
</div>
<time dateTime={post.publishedAt}>
{new Date(post.publishedAt).toLocaleDateString()}
</time>
<div className="tags">
{post.tags.map((tag) => (
<span key={tag} className="tag" itemProp="keywords">
{tag}
</span>
))}
</div>
</header>
<img
src={post.coverImage}
alt={post.title}
className="cover-image"
itemProp="image"
/>
<div
className="content"
itemProp="articleBody"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.excerpt,
image: post.coverImage,
datePublished: post.publishedAt,
dateModified: post.updatedAt,
author: {
'@type': 'Person',
name: post.author.name,
image: post.author.image,
},
keywords: post.tags.join(', '),
}),
}}
/>
</article>
)
}
// app/products/[id]/page.tsx
export async function generateMetadata({
params,
}: {
params: { id: string }
}): Promise<Metadata> {
const product = await getProduct(params.id)
return {
title: `${product.name} - Buy Now`,
description: product.description,
openGraph: {
title: product.name,
description: product.description,
type: 'website',
images: product.images,
},
other: {
'product:price:amount': product.price.toString(),
'product:price:currency': 'USD',
},
}
}---
12. Image Optimization
Advanced image optimization techniques.
// app/gallery/page.tsx
import Image from 'next/image'
interface GalleryImage {
id: string
url: string
title: string
width: number
height: number
}
async function getGalleryImages(): Promise<GalleryImage[]> {
const res = await fetch('https://api.example.com/gallery')
return res.json()
}
export default async function GalleryPage() {
const images = await getGalleryImages()
return (
<div className="gallery">
<h1>Photo Gallery</h1>
<div className="gallery-grid">
{images.map((image) => (
<div key={image.id} className="gallery-item">
<Image
src={image.url}
alt={image.title}
width={image.width}
height={image.height}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
quality={85}
placeholder="blur"
blurDataURL={generateBlurDataURL(image)}
/>
<p>{image.title}</p>
</div>
))}
</div>
</div>
)
}
// Hero image with priority loading
export function HeroSection() {
return (
<section className="hero">
<Image
src="/hero-background.jpg"
alt="Hero background"
fill
style={{ objectFit: 'cover' }}
priority
quality={90}
/>
<div className="hero-content">
<h1>Welcome to Our Site</h1>
</div>
</section>
)
}
// Responsive images with art direction
export function ResponsiveHero() {
return (
<div className="responsive-hero">
<picture>
<source
media="(max-width: 768px)"
srcSet="/hero-mobile.jpg"
/>
<source
media="(min-width: 769px)"
srcSet="/hero-desktop.jpg"
/>
<Image
src="/hero-desktop.jpg"
alt="Hero"
width={1920}
height={1080}
priority
/>
</picture>
</div>
)
}
// Product grid with lazy loading
export function ProductGrid({ products }) {
return (
<div className="product-grid">
{products.map((product, index) => (
<div key={product.id} className="product-card">
<Image
src={product.image}
alt={product.name}
width={400}
height={400}
loading={index < 4 ? 'eager' : 'lazy'}
quality={80}
/>
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
))}
</div>
)
}
// Helper function to generate blur data URL
function generateBlurDataURL(image: GalleryImage): string {
// In production, generate this server-side
return `data:image/svg+xml;base64,${Buffer.from(
`<svg width="${image.width}" height="${image.height}" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f0f0f0"/>
</svg>`
).toString('base64')}`
}
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
{
protocol: 'https',
hostname: 'cdn.example.com',
},
],
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
}---
13. Middleware for Rate Limiting
Implementing rate limiting and security with middleware.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
// Create rate limiter
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'),
analytics: true,
})
export async function middleware(request: NextRequest) {
const ip = request.ip ?? '127.0.0.1'
const { success, pending, limit, reset, remaining } = await ratelimit.limit(
`ratelimit_${ip}`
)
// Add rate limit headers
const response = success
? NextResponse.next()
: NextResponse.json(
{ error: 'Too Many Requests' },
{ status: 429 }
)
response.headers.set('X-RateLimit-Limit', limit.toString())
response.headers.set('X-RateLimit-Remaining', remaining.toString())
response.headers.set('X-RateLimit-Reset', reset.toString())
return response
}
export const config = {
matcher: '/api/:path*',
}
// middleware-auth.ts (Advanced example)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { verifySession } from './lib/auth'
const publicPaths = ['/login', '/register', '/about', '/']
const apiPublicPaths = ['/api/auth/login', '/api/auth/register']
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// Security headers
const response = NextResponse.next()
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
response.headers.set(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=()'
)
// Skip auth check for public paths
if (
publicPaths.some((path) => pathname.startsWith(path)) ||
apiPublicPaths.some((path) => pathname === path)
) {
return response
}
// Check authentication
const token = request.cookies.get('session')?.value
if (!token) {
if (pathname.startsWith('/api/')) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
return NextResponse.redirect(new URL('/login', request.url))
}
// Verify session
const session = await verifySession(token)
if (!session) {
if (pathname.startsWith('/api/')) {
return NextResponse.json({ error: 'Invalid session' }, { status: 401 })
}
return NextResponse.redirect(new URL('/login', request.url))
}
// Add user info to request headers
response.headers.set('X-User-Id', session.userId as string)
return response
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}---
14. E-commerce Product Catalog
Complete e-commerce implementation with filtering and pagination.
// app/products/page.tsx
import { Suspense } from 'react'
import ProductGrid from './ProductGrid'
import Filters from './Filters'
import Pagination from './Pagination'
interface SearchParams {
page?: string
category?: string
minPrice?: string
maxPrice?: string
sort?: string
search?: string
}
export default function ProductsPage({
searchParams,
}: {
searchParams: SearchParams
}) {
return (
<div className="products-page">
<h1>Products</h1>
<div className="products-layout">
<aside className="filters-sidebar">
<Filters />
</aside>
<div className="products-content">
<Suspense fallback={<ProductGridSkeleton />}>
<ProductGrid searchParams={searchParams} />
</Suspense>
</div>
</div>
</div>
)
}
// app/products/ProductGrid.tsx
interface Product {
id: string
name: string
price: number
image: string
category: string
rating: number
inStock: boolean
}
interface ProductsResponse {
products: Product[]
total: number
page: number
totalPages: number
}
async function getProducts(
searchParams: SearchParams
): Promise<ProductsResponse> {
const params = new URLSearchParams()
if (searchParams.page) params.set('page', searchParams.page)
if (searchParams.category) params.set('category', searchParams.category)
if (searchParams.minPrice) params.set('minPrice', searchParams.minPrice)
if (searchParams.maxPrice) params.set('maxPrice', searchParams.maxPrice)
if (searchParams.sort) params.set('sort', searchParams.sort)
if (searchParams.search) params.set('search', searchParams.search)
const res = await fetch(`https://api.example.com/products?${params}`, {
next: { revalidate: 300 }, // Revalidate every 5 minutes
})
return res.json()
}
export default async function ProductGrid({
searchParams,
}: {
searchParams: SearchParams
}) {
const data = await getProducts(searchParams)
if (data.products.length === 0) {
return (
<div className="no-products">
<p>No products found</p>
</div>
)
}
return (
<>
<div className="products-grid">
{data.products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
<Pagination
currentPage={data.page}
totalPages={data.totalPages}
total={data.total}
/>
</>
)
}
// app/products/ProductCard.tsx
import Image from 'next/image'
import Link from 'next/link'
import AddToCartButton from './AddToCartButton'
export default function ProductCard({ product }: { product: Product }) {
return (
<div className="product-card">
<Link href={`/products/${product.id}`}>
<Image
src={product.image}
alt={product.name}
width={400}
height={400}
className="product-image"
/>
</Link>
<div className="product-info">
<h3>
<Link href={`/products/${product.id}`}>{product.name}</Link>
</h3>
<div className="product-rating">
{'⭐'.repeat(Math.round(product.rating))}
<span>{product.rating}</span>
</div>
<div className="product-footer">
<span className="price">${product.price}</span>
{product.inStock ? (
<AddToCartButton productId={product.id} />
) : (
<span className="out-of-stock">Out of Stock</span>
)}
</div>
</div>
</div>
)
}
// app/products/[id]/page.tsx
import Image from 'next/image'
import { notFound } from 'next/navigation'
import AddToCartButton from '../AddToCartButton'
import RelatedProducts from './RelatedProducts'
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`)
if (!res.ok) return null
return res.json()
}
export async function generateMetadata({ params }: { params: { id: string } }) {
const product = await getProduct(params.id)
if (!product) {
return { title: 'Product Not Found' }
}
return {
title: `${product.name} - Buy Now`,
description: product.description,
}
}
export default async function ProductPage({
params,
}: {
params: { id: string }
}) {
const product = await getProduct(params.id)
if (!product) {
notFound()
}
return (
<div className="product-page">
<div className="product-details">
<div className="product-images">
<Image
src={product.image}
alt={product.name}
width={600}
height={600}
priority
/>
</div>
<div className="product-info">
<h1>{product.name}</h1>
<div className="product-rating">
{'⭐'.repeat(Math.round(product.rating))}
<span>({product.reviewCount} reviews)</span>
</div>
<p className="price">${product.price}</p>
<p className="description">{product.description}</p>
<div className="product-meta">
<p>Category: {product.category}</p>
<p>SKU: {product.sku}</p>
<p>
Availability:{' '}
{product.inStock ? 'In Stock' : 'Out of Stock'}
</p>
</div>
{product.inStock && (
<AddToCartButton productId={product.id} />
)}
</div>
</div>
<section className="related-products">
<h2>Related Products</h2>
<Suspense fallback={<div>Loading...</div>}>
<RelatedProducts category={product.category} currentId={product.id} />
</Suspense>
</section>
</div>
)
}
// app/products/AddToCartButton.tsx
'use client'
import { useState } from 'react'
export default function AddToCartButton({ productId }: { productId: string }) {
const [isAdding, setIsAdding] = useState(false)
async function handleAddToCart() {
setIsAdding(true)
try {
await fetch('/api/cart', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId, quantity: 1 }),
})
// Show success message
alert('Added to cart!')
} catch (error) {
alert('Failed to add to cart')
} finally {
setIsAdding(false)
}
}
return (
<button
onClick={handleAddToCart}
disabled={isAdding}
className="add-to-cart-button"
>
{isAdding ? 'Adding...' : 'Add to Cart'}
</button>
)
}---
15. Blog with MDX Content
Building a blog with MDX support for rich content.
// app/blog/[slug]/page.tsx
import { MDXRemote } from 'next-mdx-remote/rsc'
import { notFound } from 'next/navigation'
import { highlight } from 'sugar-high'
interface Post {
slug: string
title: string
date: string
content: string
author: string
}
async function getPost(slug: string): Promise<Post | null> {
try {
const res = await fetch(`https://api.example.com/posts/${slug}`)
if (!res.ok) return null
return res.json()
} catch {
return null
}
}
// Custom MDX components
const components = {
h1: (props) => <h1 className="text-4xl font-bold mb-4" {...props} />,
h2: (props) => <h2 className="text-3xl font-bold mb-3" {...props} />,
p: (props) => <p className="mb-4 leading-relaxed" {...props} />,
code: ({ children, ...props }) => {
const codeHTML = highlight(children)
return <code dangerouslySetInnerHTML={{ __html: codeHTML }} {...props} />
},
pre: (props) => (
<pre className="bg-gray-900 text-white p-4 rounded-lg overflow-x-auto mb-4" {...props} />
),
a: (props) => (
<a className="text-blue-600 hover:underline" {...props} />
),
ul: (props) => <ul className="list-disc list-inside mb-4" {...props} />,
ol: (props) => <ol className="list-decimal list-inside mb-4" {...props} />,
blockquote: (props) => (
<blockquote className="border-l-4 border-gray-300 pl-4 italic my-4" {...props} />
),
img: (props) => (
<img className="rounded-lg my-4 w-full" {...props} />
),
}
export default async function BlogPost({
params,
}: {
params: { slug: string }
}) {
const post = await getPost(params.slug)
if (!post) {
notFound()
}
return (
<article className="blog-post max-w-3xl mx-auto px-4 py-8">
<header className="mb-8">
<h1 className="text-5xl font-bold mb-4">{post.title}</h1>
<div className="text-gray-600">
<span>By {post.author}</span>
<span className="mx-2">•</span>
<time>{new Date(post.date).toLocaleDateString()}</time>
</div>
</header>
<div className="prose prose-lg">
<MDXRemote source={post.content} components={components} />
</div>
</article>
)
}---
16. Dashboard with Protected Routes
Complete dashboard implementation with authentication.
// app/dashboard/layout.tsx
import { getCurrentUser } from '@/lib/auth'
import { redirect } from 'next/navigation'
import DashboardNav from './DashboardNav'
import UserMenu from './UserMenu'
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const user = await getCurrentUser()
if (!user) {
redirect('/login')
}
return (
<div className="dashboard-layout">
<aside className="dashboard-sidebar">
<div className="logo">
<h1>Dashboard</h1>
</div>
<DashboardNav />
</aside>
<div className="dashboard-main">
<header className="dashboard-header">
<h2>Welcome, {user.name}</h2>
<UserMenu user={user} />
</header>
<main className="dashboard-content">{children}</main>
</div>
</div>
)
}
// app/dashboard/page.tsx
import { Suspense } from 'react'
import StatsCard from './StatsCard'
import RecentActivity from './RecentActivity'
import QuickActions from './QuickActions'
export default function DashboardPage() {
return (
<div className="dashboard-page">
<div className="stats-grid">
<Suspense fallback={<StatsCardSkeleton />}>
<StatsCard type="users" />
</Suspense>
<Suspense fallback={<StatsCardSkeleton />}>
<StatsCard type="revenue" />
</Suspense>
<Suspense fallback={<StatsCardSkeleton />}>
<StatsCard type="orders" />
</Suspense>
<Suspense fallback={<StatsCardSkeleton />}>
<StatsCard type="growth" />
</Suspense>
</div>
<div className="dashboard-grid">
<Suspense fallback={<div>Loading...</div>}>
<RecentActivity />
</Suspense>
<QuickActions />
</div>
</div>
)
}---
17. Search with Server Actions
Implementing search functionality with Server Actions.
// app/search/actions.ts
'use server'
import { redirect } from 'next/navigation'
export async function searchAction(formData: FormData) {
const query = formData.get('query') as string
if (!query) {
return { error: 'Query is required' }
}
redirect(`/search/results?q=${encodeURIComponent(query)}`)
}
// app/search/page.tsx
import { searchAction } from './actions'
import SearchForm from './SearchForm'
export default function SearchPage() {
return (
<div className="search-page">
<h1>Search</h1>
<SearchForm action={searchAction} />
</div>
)
}
// app/search/results/page.tsx
import { Suspense } from 'react'
async function searchResults(query: string) {
const res = await fetch(
`https://api.example.com/search?q=${encodeURIComponent(query)}`
)
return res.json()
}
export default function SearchResultsPage({
searchParams,
}: {
searchParams: { q: string }
}) {
return (
<div className="search-results">
<h1>Search Results for "{searchParams.q}"</h1>
<Suspense fallback={<div>Searching...</div>}>
<Results query={searchParams.q} />
</Suspense>
</div>
)
}
async function Results({ query }: { query: string }) {
const results = await searchResults(query)
return (
<div className="results-list">
{results.map((result) => (
<div key={result.id} className="result-item">
<h3>{result.title}</h3>
<p>{result.excerpt}</p>
</div>
))}
</div>
)
}---
18. Internationalization (i18n)
Implementing multi-language support.
// app/[lang]/layout.tsx
import { i18n } from '@/i18n-config'
export async function generateStaticParams() {
return i18n.locales.map((locale) => ({ lang: locale }))
}
export default function LocaleLayout({
children,
params,
}: {
children: React.ReactNode
params: { lang: string }
}) {
return (
<html lang={params.lang}>
<body>{children}</body>
</html>
)
}
// app/[lang]/page.tsx
import { getDictionary } from '@/get-dictionary'
export default async function HomePage({
params,
}: {
params: { lang: string }
}) {
const dict = await getDictionary(params.lang)
return (
<div>
<h1>{dict.home.title}</h1>
<p>{dict.home.description}</p>
</div>
)
}---
19. Real-time Updates with Server-Sent Events
Implementing real-time features.
// app/api/events/route.ts
export async function GET() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
const interval = setInterval(() => {
const data = {
time: new Date().toISOString(),
value: Math.random(),
}
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
)
}, 1000)
// Clean up on close
return () => clearInterval(interval)
},
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
})
}---
20. Advanced Caching Strategies
Implementing sophisticated caching patterns.
// app/actions.ts
'use server'
import { revalidatePath, revalidateTag } from 'next/cache'
export async function revalidateProduct(productId: string) {
revalidateTag(`product-${productId}`)
revalidatePath(`/products/${productId}`)
}
// Fetch with tags
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: { tags: [`product-${id}`, 'products'] }
})
return res.json()
}
// Revalidate all products
export async function revalidateAllProducts() {
revalidateTag('products')
}---
This examples file provides comprehensive, production-ready code samples for all major Next.js development patterns.
Next.js Development Skill
A comprehensive skill for building modern full-stack applications with Next.js App Router, Server Components, and advanced routing patterns.
Overview
This skill provides complete guidance for Next.js development using the latest App Router architecture. It covers server and client components, data fetching patterns, routing strategies, API development, middleware, and production-ready optimization techniques.
What is Next.js?
Next.js is a React framework for building full-stack web applications. It provides:
- Server-Side Rendering (SSR): Pre-render pages on each request
- Static Site Generation (SSG): Generate HTML at build time
- Incremental Static Regeneration (ISR): Update static content after build
- API Routes: Build backend API endpoints
- File-based Routing: Automatic routing based on file structure
- Automatic Optimization: Images, fonts, scripts automatically optimized
- Edge Computing: Deploy serverless functions globally
Key Features
App Router
The App Router is Next.js's modern routing system built on React Server Components:
- File-based routing in the
appdirectory - Nested layouts that preserve state
- Loading UI with React Suspense
- Error boundaries for error handling
- Parallel and intercepting routes
- Route groups for organization
Server Components
Server Components render on the server by default:
- Zero JavaScript sent to the client
- Direct access to backend resources (databases, APIs)
- Better performance and smaller bundle sizes
- Automatic code splitting
- SEO-friendly content
Data Fetching
Extended fetch API with powerful caching:
- Static:
cache: 'force-cache'- Generate at build time - Dynamic:
cache: 'no-store'- Fetch on every request - Revalidation:
revalidate: 60- Update periodically - Parallel and sequential fetching patterns
- Automatic request deduplication
Getting Started
Installation
npx create-next-app@latest my-app
cd my-app
npm run devProject Structure
my-app/
├── app/ # App Router directory
│ ├── layout.tsx # Root layout (required)
│ ├── page.tsx # Home page
│ ├── loading.tsx # Loading UI
│ ├── error.tsx # Error UI
│ ├── not-found.tsx # 404 page
│ └── api/ # API routes
│ └── hello/
│ └── route.ts
├── public/ # Static files
│ ├── images/
│ └── fonts/
├── components/ # React components
├── lib/ # Utility functions
├── styles/ # CSS files
├── next.config.js # Next.js configuration
├── package.json
└── tsconfig.jsonBasic Page
// app/page.tsx
export default function HomePage() {
return (
<main>
<h1>Welcome to Next.js</h1>
<p>Start building your application</p>
</main>
)
}Root Layout
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}Core Concepts
Server vs Client Components
Server Component (Default):
// No 'use client' directive
async function getData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
export default async function Page() {
const data = await getData()
return <div>{data.title}</div>
}Client Component:
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}Dynamic Routes
// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
return <h1>Post: {params.slug}</h1>
}
// Generate static pages at build time
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return posts.map((post) => ({ slug: post.slug }))
}API Routes
// app/api/users/route.ts
export async function GET(request: Request) {
const users = await db.user.findMany()
return Response.json(users)
}
export async function POST(request: Request) {
const body = await request.json()
const user = await db.user.create({ data: body })
return Response.json(user, { status: 201 })
}Metadata
// app/about/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'About Us',
description: 'Learn more about our company',
}
export default function AboutPage() {
return <h1>About Us</h1>
}Common Use Cases
1. Blog or Content Site
- Static generation for blog posts
- Dynamic metadata for SEO
- Image optimization for fast loading
- Incremental Static Regeneration for updates
2. E-commerce
- Product catalog with filtering
- Shopping cart with client-side state
- Checkout with server actions
- Order management API
3. Dashboard Application
- Protected routes with middleware
- Real-time data with streaming
- Complex layouts with nested routes
- API integration
4. Marketing Website
- Fully static pages for performance
- SEO optimization with metadata
- Image and font optimization
- Fast page transitions
5. SaaS Application
- Authentication and authorization
- Database integration
- Server actions for mutations
- API routes for external integrations
Performance Optimization
Image Optimization
import Image from 'next/image'
<Image
src="/hero.jpg"
alt="Hero"
width={1920}
height={1080}
priority
/>Font Optimization
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export default function RootLayout({ children }) {
return (
<html className={inter.className}>
<body>{children}</body>
</html>
)
}Code Splitting
import { lazy, Suspense } from 'react'
const HeavyComponent = lazy(() => import('./HeavyComponent'))
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
)
}Caching Strategies
// Static - cached indefinitely
fetch('https://api.example.com/data', { cache: 'force-cache' })
// Dynamic - never cached
fetch('https://api.example.com/data', { cache: 'no-store' })
// Revalidate - cached with time-based revalidation
fetch('https://api.example.com/data', { next: { revalidate: 3600 } })Deployment
Vercel (Recommended)
npm install -g vercel
vercelOther Platforms
Next.js can be deployed to:
- AWS (Amplify, ECS, Lambda)
- Google Cloud Platform
- Azure
- DigitalOcean
- Self-hosted (Docker, Node.js)
Build for Production
npm run build
npm run startConfiguration
next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
],
},
experimental: {
serverActions: true,
},
}
module.exports = nextConfigEnvironment Variables
# .env.local
DATABASE_URL="postgresql://..."
API_KEY="your-api-key"
NEXT_PUBLIC_ANALYTICS_ID="your-analytics-id"Access in code:
// Server components and API routes
const dbUrl = process.env.DATABASE_URL
// Client components (must start with NEXT_PUBLIC_)
const analyticsId = process.env.NEXT_PUBLIC_ANALYTICS_IDBest Practices
1. Use Server Components by Default: Only use Client Components when needed (interactivity, hooks, browser APIs)
2. Fetch Data Where Needed: Don't prop-drill data from parent to child components
3. Parallel Fetching: Use Promise.all() to fetch multiple data sources simultaneously
4. Optimize Images: Always use the Image component from next/image
5. Implement Error Boundaries: Add error.tsx files for graceful error handling
6. Add Loading States: Use loading.tsx files for better user experience
7. Use Metadata API: Define SEO metadata for every page
8. Implement Middleware: Use middleware for authentication, redirects, and common logic
9. Type Safety: Use TypeScript for better development experience
10. Environment Variables: Keep secrets in .env.local and never commit them
Troubleshooting
Common Issues
Error: "You're importing a component that needs useState"
Problem: Using hooks in a Server Component
Solution: Add 'use client' directive at the top of the file:
'use client'
import { useState } from 'react'Error: "Hydration mismatch"
Problem: Server and client HTML don't match
Solutions:
- Don't use browser-only APIs during initial render
- Use
useEffectfor client-only code - Check for conditional rendering based on
window
'use client'
import { useEffect, useState } from 'react'
export default function Component() {
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) return null
return <div>{window.innerWidth}</div>
}Error: "Module not found" after adding dependency
Problem: Package not installed or needs restart
Solution:
# Install the package
npm install package-name
# Restart dev server
npm run devSlow Build Times
Solutions:
- Enable SWC minifier (default in Next.js 13+)
- Use dynamic imports for large components
- Optimize images before adding to project
- Remove unused dependencies
- Consider incremental static regeneration instead of full static generation
// next.config.js
module.exports = {
swcMinify: true,
compiler: {
removeConsole: process.env.NODE_ENV === 'production',
},
}API Route Not Working
Checklist:
- File is in
app/api/directory - File is named
route.tsorroute.js - Exported function name matches HTTP method (GET, POST, etc.)
- Response is returned using
ResponseorNextResponse
// ✅ Correct
export async function GET() {
return Response.json({ message: 'Hello' })
}
// ❌ Incorrect - not exported
async function GET() {
return Response.json({ message: 'Hello' })
}Environment Variables Not Working
Common Mistakes:
- Not prefixing client-side variables with
NEXT_PUBLIC_ - Not restarting dev server after changing
.env.local - Committing
.env.localto git (should be in.gitignore)
Solution:
# Server-side only
DATABASE_URL=...
# Client and server-side
NEXT_PUBLIC_API_URL=...Images Not Optimizing
Checklist:
- Using
next/imagecomponent (not<img>) - Width and height specified
- Remote images configured in
next.config.js
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'example.com',
},
],
},
}Learning Resources
- Official Docs: https://nextjs.org/docs
- Learn Next.js: https://nextjs.org/learn
- Examples: https://github.com/vercel/next.js/tree/canary/examples
- Community: https://github.com/vercel/next.js/discussions
Related Skills
- react-development: Core React concepts and hooks
- typescript: Type safety for Next.js applications
- tailwind-css: Utility-first CSS framework
- prisma: Database ORM for Next.js applications
Version
This skill is based on Next.js 13+ with App Router. The patterns and examples follow the latest Next.js best practices and official documentation.
License
This skill documentation is part of the Claude Code skill system and follows the same licensing as the Claude Code project.