// Expert guidance for Next.js framework including App Router, Server Components, routing, data fetching, API routes, middleware, and deployment. Use this when building Next.js applications, working with React Server Components, or implementing Next.js features.
| name | Next.js |
| description | Expert guidance for Next.js framework including App Router, Server Components, routing, data fetching, API routes, middleware, and deployment. Use this when building Next.js applications, working with React Server Components, or implementing Next.js features. |
Expert assistance with Next.js React framework and modern web application development.
Next.js is a React framework with:
This guide focuses on App Router (modern approach).
# Create Next.js app (interactive)
npx create-next-app@latest
# With specific options
npx create-next-app@latest my-app --typescript --tailwind --app --use-npm
# Project structure
my-app/
โโโ app/ # App Router directory
โ โโโ layout.tsx # Root layout
โ โโโ page.tsx # Home page
โ โโโ globals.css # Global styles
โ โโโ api/ # API routes
โโโ public/ # Static assets
โโโ components/ # React components
โโโ lib/ # Utility functions
โโโ next.config.js # Next.js configuration
# Start dev server
npm run dev
# Build for production
npm run build
# Start production server
npm start
# Run linter
npm run lint
Special Files:
layout.tsx - Shared UI for a segmentpage.tsx - Unique UI for a routeloading.tsx - Loading UIerror.tsx - Error UInot-found.tsx - 404 UIroute.tsx - API endpointExample Structure:
app/
โโโ layout.tsx # Root layout
โโโ page.tsx # / route
โโโ about/
โ โโโ page.tsx # /about route
โโโ blog/
โ โโโ layout.tsx # Blog layout
โ โโโ page.tsx # /blog route
โ โโโ [slug]/
โ โโโ page.tsx # /blog/:slug route
โโโ dashboard/
โโโ layout.tsx
โโโ page.tsx # /dashboard route
โโโ settings/
โ โโโ page.tsx # /dashboard/settings
โโโ [id]/
โโโ page.tsx # /dashboard/:id
// app/layout.tsx
import type { Metadata } from 'next'
import './globals.css'
export const metadata: Metadata = {
title: 'My App',
description: 'Built with Next.js',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
// app/page.tsx
export default function Home() {
return (
<main>
<h1>Welcome to Next.js</h1>
<p>This is a Server Component by default</p>
</main>
)
}
// app/blog/[slug]/page.tsx
export default function BlogPost({
params,
}: {
params: { slug: string }
}) {
return <h1>Blog Post: {params.slug}</h1>
}
// Generate static params for SSG
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((post) => ({
slug: post.slug,
}))
}
// app/shop/[...slug]/page.tsx - Matches /shop/a, /shop/a/b, etc.
// app/docs/[[...slug]]/page.tsx - Optional catch-all, also matches /docs
export default function Page({
params,
}: {
params: { slug: string[] }
}) {
return <div>Path: {params.slug.join('/')}</div>
}
// Group routes without affecting URL structure
app/
โโโ (marketing)/
โ โโโ about/
โ โ โโโ page.tsx # /about
โ โโโ blog/
โ โโโ page.tsx # /blog
โโโ (shop)/
โโโ layout.tsx # Shop layout
โโโ products/
โ โโโ page.tsx # /products
โโโ cart/
โโโ page.tsx # /cart
// app/posts/page.tsx
// Server Component by default - runs on server
async function getPosts() {
const res = await fetch('https://api.example.com/posts')
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<div>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
</article>
))}
</div>
)
}
Benefits:
// components/Counter.tsx
'use client' // Required for client components
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>
)
}
Use when you need:
useState, useReducer)useEffect)// app/page.tsx (Server Component)
import ClientComponent from '@/components/ClientComponent'
import ServerComponent from '@/components/ServerComponent'
export default async function Page() {
const data = await fetchData()
return (
<div>
<ServerComponent data={data} />
<ClientComponent />
</div>
)
}
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // Revalidate every hour
})
if (!res.ok) throw new Error('Failed to fetch')
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return <PostsList posts={posts} />
}
// No caching (dynamic)
fetch('https://api.example.com/data', { cache: 'no-store' })
// Cache indefinitely (static)
fetch('https://api.example.com/data', { cache: 'force-cache' })
// Revalidate after time
fetch('https://api.example.com/data', {
next: { revalidate: 60 } // 60 seconds
})
// Revalidate with tag
fetch('https://api.example.com/data', {
next: { tags: ['posts'] }
})
// Then revalidate programmatically
import { revalidateTag } from 'next/cache'
revalidateTag('posts')
export default async function Page() {
// Initiate both requests in parallel
const userPromise = getUser()
const postsPromise = getPosts()
// Wait for both
const [user, posts] = await Promise.all([
userPromise,
postsPromise,
])
return <Dashboard user={user} posts={posts} />
}
export default async function Page() {
// Fetch user first
const user = await getUser()
// Then fetch user's posts
const posts = await getUserPosts(user.id)
return <Profile user={user} posts={posts} />
}
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2" />
</div>
)
}
// app/dashboard/error.tsx
'use client'
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>
)
}
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
export default async function BlogPost({
params,
}: {
params: { slug: string }
}) {
const post = await getPost(params.slug)
if (!post) {
notFound()
}
return <article>{post.content}</article>
}
// app/blog/[slug]/not-found.tsx
export default function NotFound() {
return <div>Blog post not found</div>
}
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'
// GET /api/posts
export async function GET(request: NextRequest) {
const posts = await getPosts()
return NextResponse.json(posts)
}
// POST /api/posts
export async function POST(request: NextRequest) {
const body = await request.json()
const post = await createPost(body)
return NextResponse.json(post, { status: 201 })
}
// app/api/posts/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = await getPost(params.id)
if (!post) {
return NextResponse.json(
{ error: 'Post not found' },
{ status: 404 }
)
}
return NextResponse.json(post)
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
await deletePost(params.id)
return NextResponse.json({ success: true })
}
// app/api/search/route.ts
export async function GET(request: NextRequest) {
// Query parameters
const searchParams = request.nextUrl.searchParams
const query = searchParams.get('q')
// Headers
const token = request.headers.get('authorization')
// Cookies
const session = request.cookies.get('session')
const results = await search(query)
// Set cookies in response
const response = NextResponse.json(results)
response.cookies.set('last-search', query || '', {
httpOnly: true,
secure: true,
maxAge: 3600,
})
return response
}
// middleware.ts (root level)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Check authentication
const token = request.cookies.get('token')
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Add custom header
const response = NextResponse.next()
response.headers.set('x-custom-header', 'value')
return response
}
// Configure which routes to run middleware on
export const config = {
matcher: [
'/dashboard/:path*',
'/api/:path*',
],
}
import Link from 'next/link'
export default function Nav() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog/my-post">Blog Post</Link>
{/* With prefetching disabled */}
<Link href="/heavy-page" prefetch={false}>
Heavy Page
</Link>
</nav>
)
}
'use client'
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
export default function NavigationExample() {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const handleNavigate = () => {
router.push('/dashboard')
// router.replace('/dashboard') // No history entry
// router.back()
// router.forward()
// router.refresh() // Refresh current route
}
return (
<div>
<p>Current path: {pathname}</p>
<p>Query param: {searchParams.get('id')}</p>
<button onClick={handleNavigate}>Go to Dashboard</button>
</div>
)
}
// app/about/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'About Us',
description: 'Learn more about our company',
keywords: ['about', 'company', 'team'],
openGraph: {
title: 'About Us',
description: 'Learn more about our company',
images: ['/og-image.png'],
},
twitter: {
card: 'summary_large_image',
title: 'About Us',
description: 'Learn more about our company',
images: ['/twitter-image.png'],
},
}
export default function AboutPage() {
return <div>About Us</div>
}
// app/blog/[slug]/page.tsx
export async function generateMetadata({
params,
}: {
params: { slug: string }
}): 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],
},
}
}
import Image from 'next/image'
export default function Gallery() {
return (
<div>
{/* Local image */}
<Image
src="/hero.jpg"
alt="Hero"
width={800}
height={600}
priority // Load eagerly
/>
{/* Remote image */}
<Image
src="https://example.com/image.jpg"
alt="Remote"
width={800}
height={600}
loading="lazy"
/>
{/* Fill container */}
<div className="relative h-64">
<Image
src="/background.jpg"
alt="Background"
fill
className="object-cover"
/>
</div>
</div>
)
}
// .env.local
DATABASE_URL=postgresql://...
NEXT_PUBLIC_API_URL=https://api.example.com
// Server component (private vars)
const dbUrl = process.env.DATABASE_URL
// Client component (public vars only)
const apiUrl = process.env.NEXT_PUBLIC_API_URL
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// Image domains
images: {
domains: ['example.com', 'cdn.example.com'],
},
// Redirects
async redirects() {
return [
{
source: '/old-path',
destination: '/new-path',
permanent: true,
},
]
},
// Rewrites
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'https://api.example.com/:path*',
},
]
},
// Headers
async headers() {
return [
{
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Origin', value: '*' },
],
},
]
},
// Environment variables
env: {
CUSTOM_VAR: 'value',
},
}
module.exports = nextConfig
// โ
Use Server Components when possible
// app/page.tsx
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>
}
// โ Don't use Client Component unnecessarily
'use client'
export default function Page() {
// This doesn't need to be a Client Component
return <div>Static content</div>
}
// โ
Fetch in Server Components
async function Page() {
const data = await fetch('...').then(r => r.json())
return <List data={data} />
}
// โ Don't fetch in Client Components if avoidable
'use client'
function Page() {
const [data, setData] = useState([])
useEffect(() => {
fetch('...').then(r => r.json()).then(setData)
}, [])
return <List data={data} />
}
// โ
Use loading.tsx for automatic loading UI
// app/dashboard/loading.tsx
export default function Loading() {
return <Skeleton />
}
// โ
Or use Suspense for granular control
import { Suspense } from 'react'
export default function Page() {
return (
<Suspense fallback={<Skeleton />}>
<DataComponent />
</Suspense>
)
}
// โ
Use error.tsx for error boundaries
// app/dashboard/error.tsx
'use client'
export default function Error({ error, reset }) {
return (
<div>
<h2>Error: {error.message}</h2>
<button onClick={reset}>Retry</button>
</div>
)
}
// โ
Always add metadata for SEO
export const metadata = {
title: 'Page Title',
description: 'Page description',
}
// โ
Use dynamic metadata for dynamic routes
export async function generateMetadata({ params }) {
const data = await getData(params.id)
return { title: data.title }
}
// middleware.ts
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
// app/api/protected/route.ts
export async function GET(request: NextRequest) {
const token = request.headers.get('authorization')
if (!token) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
const data = await getProtectedData()
return NextResponse.json(data)
}
'use client'
export default function ContactForm() {
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const formData = new FormData(e.currentTarget)
const res = await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(Object.fromEntries(formData)),
headers: { 'Content-Type': 'application/json' },
})
if (res.ok) {
alert('Submitted!')
}
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit">Submit</button>
</form>
)
}
# Install Vercel CLI
npm i -g vercel
# Deploy
vercel
# Production deployment
vercel --prod
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:18-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]