| 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. |
Next.js
Expert assistance with Next.js React framework and modern web application development.
Overview
Next.js is a React framework with:
- App Router (Next.js 13+): File-based routing with React Server Components
- Pages Router (Legacy): Traditional Next.js routing
- Server-side rendering (SSR)
- Static site generation (SSG)
- API routes
- Built-in optimization
This guide focuses on App Router (modern approach).
Project Setup
Create New Project
npx create-next-app@latest
npx create-next-app@latest my-app --typescript --tailwind --app --use-npm
my-app/
├── app/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── globals.css
│ └── api/
├── public/
├── components/
├── lib/
└── next.config.js
Development Commands
npm run dev
npm run build
npm start
npm run lint
App Router (Next.js 13+)
File-Based Routing
Special Files:
layout.tsx - Shared UI for a segment
page.tsx - Unique UI for a route
loading.tsx - Loading UI
error.tsx - Error UI
not-found.tsx - 404 UI
route.tsx - API endpoint
Example 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
Root Layout
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>
)
}
Basic Page
export default function Home() {
return (
<main>
<h1>Welcome to Next.js</h1>
<p>This is a Server Component by default</p>
</main>
)
}
Dynamic Routes
export default function BlogPost({
params,
}: {
params: { slug: string }
}) {
return <h1>Blog Post: {params.slug}</h1>
}
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((post) => ({
slug: post.slug,
}))
}
Catch-All Routes
export default function Page({
params,
}: {
params: { slug: string[] }
}) {
return <div>Path: {params.slug.join('/')}</div>
}
Route Groups
app/
├── (marketing)/
│ ├── about/
│ │ └── page.tsx # /about
│ └── blog/
│ └── page.tsx # /blog
└── (shop)/
├── layout.tsx # Shop layout
├── products/
│ └── page.tsx # /products
└── cart/
└── page.tsx # /cart
Server vs Client Components
Server Components (Default)
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:
- Access to backend resources
- Keep sensitive data on server
- Reduce client-side JavaScript
- Better performance
Client Components
'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>
)
}
Use when you need:
- State (
useState, useReducer)
- Effects (
useEffect)
- Browser APIs
- Event listeners
- Custom hooks
Composition Pattern
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>
)
}
Data Fetching
Server Component Data Fetching
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }
})
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} />
}
Caching Strategies
fetch('https://api.example.com/data', { cache: 'no-store' })
fetch('https://api.example.com/data', { cache: 'force-cache' })
fetch('https://api.example.com/data', {
next: { revalidate: 60 }
})
fetch('https://api.example.com/data', {
next: { tags: ['posts'] }
})
import { revalidateTag } from 'next/cache'
revalidateTag('posts')
Parallel Data Fetching
export default async function Page() {
const userPromise = getUser()
const postsPromise = getPosts()
const [user, posts] = await Promise.all([
userPromise,
postsPromise,
])
return <Dashboard user={user} posts={posts} />
}
Sequential Data Fetching
export default async function Page() {
const user = await getUser()
const posts = await getUserPosts(user.id)
return <Profile user={user} posts={posts} />
}
Loading & Error States
Loading UI
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>
)
}
Error Handling
'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>
)
}
Not Found
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>
}
export default function NotFound() {
return <div>Blog post not found</div>
}
API Routes
Route Handlers
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const posts = await getPosts()
return NextResponse.json(posts)
}
export async function POST(request: NextRequest) {
const body = await request.json()
const post = await createPost(body)
return NextResponse.json(post, { status: 201 })
}
Dynamic API Routes
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 })
}
Request Helpers
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const query = searchParams.get('q')
const token = request.headers.get('authorization')
const session = request.cookies.get('session')
const results = await search(query)
const response = NextResponse.json(results)
response.cookies.set('last-search', query || '', {
httpOnly: true,
secure: true,
maxAge: 3600,
})
return response
}
Middleware
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
const response = NextResponse.next()
response.headers.set('x-custom-header', 'value')
return response
}
export const config = {
matcher: [
'/dashboard/:path*',
'/api/:path*',
],
}
Navigation
Link Component
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>
)
}
Programmatic Navigation
'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')
}
return (
<div>
<p>Current path: {pathname}</p>
<p>Query param: {searchParams.get('id')}</p>
<button onClick={handleNavigate}>Go to Dashboard</button>
</div>
)
}
Metadata & SEO
Static Metadata
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>
}
Dynamic Metadata
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],
},
}
}
Image Optimization
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>
)
}
Environment Variables
DATABASE_URL=postgresql:
NEXT_PUBLIC_API_URL=https:
const dbUrl = process.env.DATABASE_URL
const apiUrl = process.env.NEXT_PUBLIC_API_URL
Configuration
const nextConfig = {
images: {
domains: ['example.com', 'cdn.example.com'],
},
async redirects() {
return [
{
source: '/old-path',
destination: '/new-path',
permanent: true,
},
]
},
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'https://api.example.com/:path*',
},
]
},
async headers() {
return [
{
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Origin', value: '*' },
],
},
]
},
env: {
CUSTOM_VAR: 'value',
},
}
module.exports = nextConfig
Best Practices
1. Server Components by Default
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>
}
'use client'
export default function Page() {
return <div>Static content</div>
}
2. Proper Data Fetching
async function Page() {
const data = await fetch('...').then(r => r.json())
return <List data={data} />
}
'use client'
function Page() {
const [data, setData] = useState([])
useEffect(() => {
fetch('...').then(r => r.json()).then(setData)
}, [])
return <List data={data} />
}
3. Loading States
export default function Loading() {
return <Skeleton />
}
import { Suspense } from 'react'
export default function Page() {
return (
<Suspense fallback={<Skeleton />}>
<DataComponent />
</Suspense>
)
}
4. Error Handling
'use client'
export default function Error({ error, reset }) {
return (
<div>
<h2>Error: {error.message}</h2>
<button onClick={reset}>Retry</button>
</div>
)
}
5. Metadata
export const metadata = {
title: 'Page Title',
description: 'Page description',
}
export async function generateMetadata({ params }) {
const data = await getData(params.id)
return { title: data.title }
}
Common Patterns
Authentication Check
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))
}
}
Protected API Route
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)
}
Form Handling
'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>
)
}
Deployment
Vercel (Recommended)
npm i -g vercel
vercel
vercel --prod
Docker
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"]
Resources