Skip to main content

Data Fetching

SSR, SSG, ISR, API routes, and data fetching strategies for Next.js applications.

App Router Data Fetching

Server Components (Default)

// app/posts/page.tsx - Server Component
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
cache: 'force-cache', // Default: cache forever
});

if (!res.ok) {
throw new Error('Failed to fetch posts');
}

return res.json();
}

export default async function PostsPage() {
const posts = await getPosts();

return (
<div>
<h1>Posts</h1>
{posts.map((post: any) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
</div>
);
}

Cache Options

// app/lib/data.ts
export async function getStaticData() {
// Cache forever (default)
const res = await fetch('https://api.example.com/data', {
cache: 'force-cache',
});
return res.json();
}

export async function getRealtimeData() {
// Never cache
const res = await fetch('https://api.example.com/realtime', {
cache: 'no-store',
});
return res.json();
}

export async function getTimedData() {
// Cache for 60 seconds
const res = await fetch('https://api.example.com/timed', {
next: { revalidate: 60 },
});
return res.json();
}

export async function getTaggedData() {
// Cache with tags for revalidation
const res = await fetch('https://api.example.com/tagged', {
next: { tags: ['posts'] },
});
return res.json();
}

Parallel Data Fetching

// app/dashboard/page.tsx
async function getUser() {
const res = await fetch('https://api.example.com/user');
return res.json();
}

async function getPosts() {
const res = await fetch('https://api.example.com/posts');
return res.json();
}

async function getAnalytics() {
const res = await fetch('https://api.example.com/analytics');
return res.json();
}

export default async function DashboardPage() {
// Parallel data fetching
const [user, posts, analytics] = await Promise.all([
getUser(),
getPosts(),
getAnalytics()
]);

return (
<div>
<h1>Welcome, {user.name}</h1>
<div className="grid grid-cols-2 gap-4">
<div>
<h2>Recent Posts</h2>
{posts.map((post: any) => (
<div key={post.id}>{post.title}</div>
))}
</div>
<div>
<h2>Analytics</h2>
<p>Views: {analytics.views}</p>
<p>Clicks: {analytics.clicks}</p>
</div>
</div>
</div>
);
}

Sequential Data Fetching

// app/post/[id]/page.tsx
async function getPost(id: string) {
const res = await fetch(`https://api.example.com/posts/${id}`);
return res.json();
}

async function getComments(postId: string) {
const res = await fetch(`https://api.example.com/posts/${postId}/comments`);
return res.json();
}

export default async function PostPage({ params }: { params: { id: string } }) {
// Sequential: get post first, then comments
const post = await getPost(params.id);
const comments = await getComments(post.id);

return (
<div>
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>

<section>
<h2>Comments</h2>
{comments.map((comment: any) => (
<div key={comment.id}>
<p>{comment.content}</p>
<small>By {comment.author}</small>
</div>
))}
</section>
</div>
);
}

Client-Side Data Fetching

SWR (Stale-While-Revalidate)

// app/components/UserProfile.tsx
'use client';

import useSWR from 'swr';

const fetcher = (url: string) => fetch(url).then((res) => res.json());

export default function UserProfile({ userId }: { userId: string }) {
const { data, error, isLoading } = useSWR(
`/api/users/${userId}`,
fetcher,
{
refreshInterval: 1000, // Refresh every second
revalidateOnFocus: true,
revalidateOnReconnect: true,
}
);

if (error) return <div>Failed to load user</div>;
if (isLoading) return <div>Loading...</div>;

return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
}

React Query (TanStack Query)

// app/components/PostList.tsx
'use client';

import { useQuery } from '@tanstack/react-query';

async function fetchPosts() {
const response = await fetch('/api/posts');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
}

export default function PostList() {
const {
data: posts,
error,
isLoading,
refetch
} = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
});

if (isLoading) return <div>Loading posts...</div>;
if (error) return <div>Error: {error.message}</div>;

return (
<div>
<button onClick={() => refetch()}>Refresh Posts</button>
{posts.map((post: any) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}

useEffect Data Fetching

// app/components/ProductList.tsx
'use client';

import { useState, useEffect } from 'react';

interface Product {
id: string;
name: string;
price: number;
}

export default function ProductList() {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
async function fetchProducts() {
try {
setLoading(true);
const response = await fetch('/api/products');

if (!response.ok) {
throw new Error('Failed to fetch products');
}

const data = await response.json();
setProducts(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
}

fetchProducts();
}, []);

if (loading) return <div>Loading products...</div>;
if (error) return <div>Error: {error}</div>;

return (
<div>
{products.map(product => (
<div key={product.id}>
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
))}
</div>
);
}

Pages Router Data Fetching

getServerSideProps (SSR)

// pages/posts/[id].tsx
import { GetServerSideProps } from 'next';

interface Post {
id: string;
title: string;
content: string;
}

interface Props {
post: Post;
}

export default function PostPage({ post }: Props) {
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}

export const getServerSideProps: GetServerSideProps = async (context) => {
const { id } = context.params!;

try {
const res = await fetch(`https://api.example.com/posts/${id}`);
const post = await res.json();

return {
props: {
post,
},
};
} catch (error) {
return {
notFound: true,
};
}
};

getStaticProps (SSG)

// pages/blog/index.tsx
import { GetStaticProps } from 'next';

interface Post {
id: string;
title: string;
excerpt: string;
}

interface Props {
posts: Post[];
}

export default function BlogPage({ posts }: Props) {
return (
<div>
<h1>Blog Posts</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}

export const getStaticProps: GetStaticProps = async () => {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();

return {
props: {
posts,
},
revalidate: 60, // Revalidate every 60 seconds
};
};

getStaticPaths (Dynamic SSG)

// pages/blog/[slug].tsx
import { GetStaticProps, GetStaticPaths } from 'next';

interface Post {
slug: string;
title: string;
content: string;
}

interface Props {
post: Post;
}

export default function BlogPost({ post }: Props) {
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}

export const getStaticPaths: GetStaticPaths = async () => {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();

const paths = posts.map((post: Post) => ({
params: { slug: post.slug },
}));

return {
paths,
fallback: 'blocking', // or false, true
};
};

export const getStaticProps: GetStaticProps = async ({ params }) => {
const { slug } = params!;
const res = await fetch(`https://api.example.com/posts/${slug}`);
const post = await res.json();

return {
props: {
post,
},
revalidate: 60,
};
};

API Routes

App Router API Routes

// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const page = searchParams.get('page') || '1';
const limit = searchParams.get('limit') || '10';

try {
const posts = await fetch(
`https://api.example.com/posts?page=${page}&limit=${limit}`
);
const data = await posts.json();

return NextResponse.json(data);
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch posts' },
{ status: 500 }
);
}
}

export async function POST(request: NextRequest) {
try {
const body = await request.json();

const response = await fetch('https://api.example.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});

const data = await response.json();

return NextResponse.json(data, { status: 201 });
} catch (error) {
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 } }
) {
const { id } = params;

try {
const response = await fetch(`https://api.example.com/posts/${id}`);
const post = await response.json();

return NextResponse.json(post);
} catch (error) {
return NextResponse.json({ error: 'Post not found' }, { status: 404 });
}
}

export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const { id } = params;
const body = await request.json();

try {
const response = await fetch(`https://api.example.com/posts/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});

const post = await response.json();

return NextResponse.json(post);
} catch (error) {
return NextResponse.json(
{ error: 'Failed to update post' },
{ status: 500 }
);
}
}

export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const { id } = params;

try {
await fetch(`https://api.example.com/posts/${id}`, {
method: 'DELETE',
});

return NextResponse.json({ message: 'Post deleted' });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to delete post' },
{ status: 500 }
);
}
}

Pages Router API Routes

// pages/api/posts/index.ts
import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === 'GET') {
const { page = '1', limit = '10' } = req.query;

try {
const response = await fetch(
`https://api.example.com/posts?page=${page}&limit=${limit}`
);
const posts = await response.json();

res.status(200).json(posts);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch posts' });
}
} else if (req.method === 'POST') {
try {
const response = await fetch('https://api.example.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(req.body),
});

const post = await response.json();

res.status(201).json(post);
} catch (error) {
res.status(500).json({ error: 'Failed to create post' });
}
} else {
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}

Database Integration

Prisma Integration

// lib/prisma.ts
import { PrismaClient } from '@prisma/client';

const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};

export const prisma = globalForPrisma.prisma ?? new PrismaClient();

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
// app/api/users/route.ts
import { prisma } from '@/lib/prisma';
import { NextResponse } from 'next/server';

export async function GET() {
try {
const users = await prisma.user.findMany({
include: {
posts: true,
},
});

return NextResponse.json(users);
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch users' },
{ status: 500 }
);
}
}

export async function POST(request: Request) {
try {
const body = await request.json();

const user = await prisma.user.create({
data: {
name: body.name,
email: body.email,
},
});

return NextResponse.json(user, { status: 201 });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to create user' },
{ status: 500 }
);
}
}

MongoDB Integration

// lib/mongodb.ts
import { MongoClient } from 'mongodb';

const uri = process.env.MONGODB_URI!;
const options = {};

let client: MongoClient;
let clientPromise: Promise<MongoClient>;

if (process.env.NODE_ENV === 'development') {
const globalWithMongo = global as typeof globalThis & {
_mongoClientPromise?: Promise<MongoClient>;
};

if (!globalWithMongo._mongoClientPromise) {
client = new MongoClient(uri, options);
globalWithMongo._mongoClientPromise = client.connect();
}
clientPromise = globalWithMongo._mongoClientPromise;
} else {
client = new MongoClient(uri, options);
clientPromise = client.connect();
}

export default clientPromise;
// app/api/products/route.ts
import clientPromise from '@/lib/mongodb';
import { NextResponse } from 'next/server';

export async function GET() {
try {
const client = await clientPromise;
const db = client.db('ecommerce');
const products = await db.collection('products').find({}).toArray();

return NextResponse.json(products);
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch products' },
{ status: 500 }
);
}
}

Error Handling

Client-Side Error Handling

// app/components/ErrorBoundary.tsx
'use client';

import { useState, useEffect } from 'react';

interface ErrorBoundaryProps {
children: React.ReactNode;
fallback?: React.ReactNode;
}

export default function ErrorBoundary({ children, fallback }: ErrorBoundaryProps) {
const [hasError, setHasError] = useState(false);

useEffect(() => {
const handleError = (error: ErrorEvent) => {
console.error('Error caught by boundary:', error);
setHasError(true);
};

window.addEventListener('error', handleError);

return () => {
window.removeEventListener('error', handleError);
};
}, []);

if (hasError) {
return fallback || <div>Something went wrong!</div>;
}

return <>{children}</>;
}

API Error Handling

// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const { id } = params;

if (!id) {
return NextResponse.json(
{ error: 'Post ID is required' },
{ status: 400 }
);
}

const post = await fetch(`https://api.example.com/posts/${id}`);

if (!post.ok) {
if (post.status === 404) {
return NextResponse.json({ error: 'Post not found' }, { status: 404 });
}

throw new Error('Failed to fetch post');
}

const data = await post.json();

return NextResponse.json(data);
} catch (error) {
console.error('API Error:', error);

return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

Quick Reference

Data Fetching Methods

  • Server Components: Default in App Router, fetch at build/request time
  • Client Components: Use SWR, React Query, or useEffect
  • getServerSideProps: Pages Router SSR
  • getStaticProps: Pages Router SSG
  • getStaticPaths: Dynamic SSG routes

Cache Options (App Router)

  • cache: 'force-cache' - Cache forever (default)
  • cache: 'no-store' - Never cache
  • next: { revalidate: 60 } - Revalidate every 60 seconds
  • next: { tags: ['posts'] } - Tag-based revalidation

API Routes

  • App Router: app/api/route.ts
  • Pages Router: pages/api/route.ts
  • Support GET, POST, PUT, DELETE methods
  • Handle request/response with Next.js utilities

Best Practices

  • Use Server Components for initial data
  • Implement proper error handling
  • Use parallel fetching when possible
  • Cache appropriately for performance
  • Validate API inputs and outputs
  • Use TypeScript for type safety
  • Handle loading and error states