Next Data Fetching¶
Server Components fetch data on the server. Route handlers build APIs. Server Actions handle mutations. Three tools , three failure modes , one framework that blurs the line between them until you leak something
Data fetching in Next.js App Router is fundamentally different from any React framework before it. Your components can await fetch() directly in the render function. No useEffect. No Redux. No React Query. The server handles it all - unless you explicitly push data fetching to the client
fetching in Server Components¶
The simplest and most secure way to get data
// src/app/products/page.tsx
export default async function ProductsPage() {
// This runs on the server only
// Never ships to the client bundle
const res = await fetch('https://api.example.com/products')
const products = await res.json()
return (
<ul>
{products.map((p: any) => (
<li key={p.id}>{p.name} - ${p.price}</li>
))}
</ul>
)
}
Key behaviors: * Server-only - the fetch call , the API key , the database connection string never reach the browser * Automatic deduplication - same fetch URL called multiple times in the same render pass gets deduplicated * Caching - fetch responses are cached by default (change with cache or next.revalidate)
// caching options
fetch('https://api.example.com/data') // cached (SSG behavior)
fetch('https://api.example.com/data', { cache: 'no-store' }) // not cached (SSR behavior)
fetch('https://api.example.com/data', { next: { revalidate: 60 } }) // ISR with 60s revalidation
route handlers - the API layer¶
Sometimes you need a traditional API endpoint. That's what route handlers are for
// src/app/api/products/route.ts
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function GET() {
const products = await prisma.product.findMany()
return NextResponse.json(products)
}
export async function POST(request: Request) {
const body = await request.json()
const product = await prisma.product.create({ data: body })
return NextResponse.json(product, { status: 201 })
}
Route handlers live in route.ts files inside the app/api/ directory
// src/app/api/products/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const product = await prisma.product.findUnique({ where: { id } })
if (!product) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
return NextResponse.json(product)
}
Route handlers support: * NextRequest - extends Request with cookies , geolocation , and URL parsing * NextResponse - extends Response with cookie and header manipulation * Dynamic segments - same [param] syntax as pages * Streaming - return a ReadableStream for large responses
Server Actions - mutations made simple¶
Server Actions are functions that run on the server but can be called from the client
// src/app/products/page.tsx
export default function ProductsPage() {
async function createProduct(formData: FormData) {
'use server'
const name = formData.get('name')
const price = formData.get('price')
// validate on the server (not in the browser!)
if (!name || !price) {
throw new Error('Name and price required')
}
await prisma.product.create({
data: { name, price: parseFloat(price as string) }
})
// revalidate the products page cache
revalidatePath('/products')
}
return (
<form action={createProduct}>
<input name="name" placeholder="Product name" />
<input name="price" placeholder="Price" type="number" step="0.01" />
<button type="submit">Create</button>
</form>
)
}
The 'use server' directive marks this function as a Server Action. It receives FormData from the form submission and runs entirely on the server
revalidation strategies¶
After mutating data , you need to tell Next.js to refresh the cached data
import { revalidatePath } from 'next/cache'
import { revalidateTag } from 'next/cache'
// revalidate by path
revalidatePath('/products') // revalidates the /products page
revalidatePath('/products/[id]') // revalidates all dynamic product pages
revalidatePath('/api/products') // revalidates the API route
// revalidate by tag
fetch('https://api.example.com/data', { next: { tags: ['products'] } })
// ...
revalidateTag('products') // revalidates all fetches tagged with 'products'
Tags are more precise. If only one product changes , you can revalidate just that tag instead of purging the entire path cache
error handling¶
Data fetching can fail. Handle it gracefully
// src/app/products/page.tsx
export default async function ProductsPage() {
let products
try {
const res = await fetch('https://api.example.com/products')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
products = await res.json()
} catch (error) {
// Log server-side but show user-friendly message
console.error('Failed to fetch products:', error)
return <div>Failed to load products. Try again later</div>
}
return <ul>{/* render products */}</ul>
}
For more granular error handling , use error.tsx:
// src/app/products/error.tsx - client component
'use client'
export default function Error({
error,
reset
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Something broke</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
The error.tsx file catches errors in Server Components and shows a UI. reset() re-renders the segment , potentially recovering from transient failures
prerequisites¶
next_05_rendering.md - you should understand SSR , SSG , and ISR strategies
next → next_07_server_actions.md