How to Fetch Data in Nextjs
How to Fetch Data in Next.js Next.js has revolutionized the way developers build modern web applications by combining the power of React with server-side rendering, static generation, and a robust data-fetching model. One of the most critical aspects of building dynamic, performant applications in Next.js is understanding how to fetch data effectively. Whether you’re pulling content from a REST AP
How to Fetch Data in Next.js
Next.js has revolutionized the way developers build modern web applications by combining the power of React with server-side rendering, static generation, and a robust data-fetching model. One of the most critical aspects of building dynamic, performant applications in Next.js is understanding how to fetch data effectively. Whether youre pulling content from a REST API, querying a GraphQL endpoint, or loading data from a database, Next.js provides multiple built-in methods tailored for different use cases. Mastering data fetching in Next.js isnt just about making HTTP requestsits about optimizing performance, improving SEO, reducing client-side load, and delivering a seamless user experience. This comprehensive guide will walk you through every essential method of fetching data in Next.js, from the basics to advanced patterns, best practices, real-world examples, and tools to streamline your workflow.
Step-by-Step Guide
Understanding Data Fetching in Next.js
Before diving into code, its crucial to understand the core philosophy behind data fetching in Next.js. Unlike traditional React applications that rely heavily on client-side data fetching using useEffect and fetch or Axios, Next.js offers server-side and static data-fetching options that render content before the page is sent to the browser. This results in faster initial loads, better SEO, and improved Core Web Vitals.
Next.js provides three primary data-fetching methods:
- getStaticProps Fetches data at build time for static generation.
- getServerSideProps Fetches data on every request for server-side rendering.
- Client-side fetching Uses React hooks like useEffect with fetch or Axios for dynamic, user-triggered data.
Each method serves a distinct purpose and should be chosen based on your applications requirements for performance, freshness, and interactivity.
Method 1: Using getStaticProps for Static Generation
getStaticProps is ideal for pages that display content that doesnt change frequentlysuch as blog posts, product listings, or marketing pages. Data is fetched at build time and embedded into the HTML, making the page fully static and extremely fast to load.
Heres a step-by-step implementation:
- Create a new page file in the
pagesdirectory (e.g.,pages/blog.js). - Export an async function named
getStaticProps. - Inside the function, fetch your data using
fetch()or any HTTP client. - Return an object with a
propskey containing the fetched data. - Use the props in your component to render the content.
Example:
jsx
// pages/blog.js
import { useState } from 'react';
export default function Blog({ posts }) {
return (
My Blog
{posts.map(post => (
{post.title}
{post.excerpt}
))}
);
}
export async function getStaticProps() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await res.json();
return {
props: {
posts,
},
};
}
When you run npm run build, Next.js will execute getStaticProps during the build process, fetch all posts from the API, and embed them directly into the HTML. This means when a user visits the page, they receive fully rendered content instantlyno JavaScript hydration delay.
For dynamic routes (e.g., pages/blog/[slug].js), you can combine getStaticProps with getStaticPaths to generate static pages for each blog post at build time.
Method 2: Using getServerSideProps for Server-Side Rendering
getServerSideProps is used when your data changes frequently or is personalized per user (e.g., dashboards, authenticated content, real-time analytics). Unlike getStaticProps, this function runs on every request, meaning the page is rendered on the server each time a user visits.
Implementation steps:
- Create a page file (e.g.,
pages/dashboard.js). - Export an async function named
getServerSideProps. - Fetch data inside the functionthis can include checking cookies, headers, or user sessions.
- Return an object with a
propskey. - Render the component using the received props.
Example:
jsx
// pages/dashboard.js
import { useState } from 'react';
export default function Dashboard({ user, balance }) {
return (
Dashboard
Welcome, {user.name}
Your balance: ${balance}
);
}
export async function getServerSideProps(context) {
const { req } = context;
// Simulate fetching user from a session or auth token
const token = req.headers.cookie?.split('; ').find(row => row.startsWith('token='))?.split('=')[1];
if (!token) {
return {
redirect: {
destination: '/login',
permanent: false,
},
};
}
const userRes = await fetch('https://api.example.com/user', {
headers: { Authorization: Bearer ${token} },
});
const user = await userRes.json();
const balanceRes = await fetch('https://api.example.com/balance', {
headers: { Authorization: Bearer ${token} },
});
const balance = await balanceRes.json();
return {
props: {
user,
balance,
},
};
}
Here, the page is regenerated on every request, ensuring the user sees the most up-to-date information. However, this comes at the cost of slightly slower load times compared to static pages, since the server must process the request each time.
Method 3: Client-Side Data Fetching with React Hooks
While server-side and static fetching are preferred for initial page loads, there are scenarios where you need to fetch data after the page has loadedsuch as user interactions (searches, filters, pagination), real-time updates, or data that depends on client state.
Next.js fully supports client-side data fetching using standard React hooks. The most common approach is using useState and useEffect with the native fetch API or libraries like Axios.
Example:
jsx
// pages/search.js
import { useState, useEffect } from 'react';
export default function Search() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!query) return;
const fetchResults = async () => {
setLoading(true);
const res = await fetch(/api/search?q=${encodeURIComponent(query)});
const data = await res.json();
setResults(data);
setLoading(false);
};
fetchResults();
}, [query]);
return (
Search Products
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
/> {loading &&
Loading...}
- {product.name}
{results.map(product => (
))}
);
}
This approach is perfect for dynamic interfaces but should be used sparingly for critical content. Search results, filters, or user comments are good candidates. Avoid using client-side fetching for content that needs to be indexed by search engines or displayed immediately on first load.
Method 4: Using SWR for Client-Side Data Fetching
While the native fetch API works, it lacks features like caching, revalidation, and loading states out of the box. For advanced client-side data fetching, SWR (Stale-While-Revalidate) by Vercel is the recommended library for Next.js applications.
SWR simplifies data fetching with built-in caching, background revalidation, and automatic refetching on focus or network reconnection.
Install SWR:
bash
npm install swr
Example using SWR:
jsx
// pages/users.js
import useSWR from 'swr';
const fetcher = (...args) => fetch(...args).then(res => res.json());
export default function Users() {
const { data, error } = useSWR('/api/users', fetcher);
if (error) return
if (!data) return
return (
- {user.name}
{data.map(user => (
))}
);
}
SWR automatically caches responses and revalidates them in the background, ensuring your UI stays fresh without requiring manual state management. Its especially powerful for dashboards, real-time feeds, or any data that needs to stay updated without full page reloads.
Method 5: API Routes for Backend Logic
Next.js allows you to create API endpoints within your application using pages/api. This is ideal for handling authentication, proxying external APIs, or creating custom logic without spinning up a separate backend server.
Example: Create a file at pages/api/search.js:
js
// pages/api/search.js
export default function handler(req, res) {
const { q } = req.query;
// Simulate search results
const results = [
{ id: 1, name: 'Next.js Documentation' },
{ id: 2, name: 'React Hooks Guide' },
{ id: 3, name: 'TypeScript Best Practices' },
].filter(item =>
item.name.toLowerCase().includes(q.toLowerCase())
);
res.status(200).json(results);
}
Now you can fetch from this endpoint client-side:
jsx
const { data } = useSWR(/api/search?q=${query}, fetcher);
This keeps your API logic colocated with your frontend, making development faster and deployment simpler.
Best Practices
Choose the Right Data-Fetching Strategy
Dont default to client-side fetching. Always ask: Does this content need to be SEO-friendly? Does it change frequently? Is it user-specific?
- Use
getStaticPropsfor content that rarely changes (blogs, docs, product catalogs). - Use
getServerSidePropsfor personalized or real-time data (dashboards, user profiles). - Use client-side fetching (SWR or fetch) for interactive UI elements (search, filters, comments).
Combining these methods intelligently results in the best performance and user experience.
Use TypeScript for Type Safety
TypeScript helps prevent runtime errors and improves developer experience. Define interfaces for your data shapes:
ts
interface Post {
id: number;
title: string;
excerpt: string;
}
export async function getStaticProps() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts: Post[] = await res.json();
return {
props: {
posts,
},
};
}
This ensures your components receive correctly typed props and reduces bugs during development.
Implement Error Boundaries and Loading States
Always handle network failures gracefully. Use loading spinners, fallback UIs, or error messages to improve UX.
With SWR:
jsx
const { data, error } = useSWR('/api/data', fetcher);
if (error) return
if (!data) return
With getServerSideProps or getStaticProps, return an error object:
js
export async function getStaticProps() {
try {
const res = await fetch('https://api.example.com/data');
if (!res.ok) throw new Error('Failed to fetch data');
const data = await res.json();
return { props: { data } };
} catch (error) {
return {
notFound: true, // Renders 404 page
};
}
}
Optimize with Incremental Static Regeneration (ISR)
Next.js 9.5+ introduced Incremental Static Regeneration (ISR), allowing you to update static pages after build without a full rebuild. This is perfect for content that updates occasionally but still benefits from static performance.
Example:
js
export async function getStaticProps() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return {
props: { posts },
revalidate: 60, // Regenerate page every 60 seconds
};
}
With ISR, the first visitor sees a cached version, and subsequent visitors may see a stale version until the page regenerates in the background. This balances performance with freshness.
Use Environment Variables for API Keys
Never hardcode API keys or secrets in your code. Use environment variables:
Create a .env.local file:
API_URL=https://api.example.com
API_KEY=your-secret-key
Access in your code:
js
const res = await fetch(${process.env.API_URL}/posts, {
headers: { Authorization: Bearer ${process.env.API_KEY} },
});
Only expose variables prefixed with NEXT_PUBLIC_ to the browser. Others remain server-side only.
Minimize Data Payloads
Fetch only the data you need. Use query parameters or GraphQL to reduce bandwidth and improve speed.
Instead of fetching an entire user object:
js
fetch('/api/user') // Returns { id, name, email, address, phone, preferences, ... }
Fetch only whats needed:
js
fetch('/api/user?fields=id,name,email')
This reduces load times and improves cache efficiency.
Tools and Resources
SWR (Stale-While-Revalidate)
SWR is the de facto standard for client-side data fetching in Next.js. Developed by Vercel, its lightweight, battle-tested, and integrates seamlessly with React. Features include:
- Automatic caching
- Background revalidation
- Focus and network revalidation
- Pagination and mutations
Documentation: swr.vercel.app
React Query
An alternative to SWR, React Query offers advanced caching, deduplication, and mutation handling. Its more feature-rich and suitable for complex applications with heavy data dependencies.
Documentation: tanstack.com/query
GraphQL with Apollo Client
For applications using GraphQL, Apollo Client integrates beautifully with Next.js. It supports server-side rendering, static generation, and client-side caching with a single API.
Documentation: apollographql.com/docs/react
Postman and Insomnia
Use these tools to test your API endpoints before integrating them into your Next.js app. They help validate responses, headers, and authentication flows.
Next.js Analytics and Performance Monitoring
Use Next.jss built-in next/dynamic for code splitting and next/font for optimized typography. Monitor performance using Lighthouse, Web Vitals, and Vercels analytics dashboard.
Mock API Tools
- JSONPlaceholder Free fake REST API for testing: jsonplaceholder.typicode.com
- Mocky Create custom mock responses: mocky.io
- MSW (Mock Service Worker) Intercept network requests in development: mswjs.io
VS Code Extensions
- ESLint Enforce code quality
- Prettier Auto-format code
- Next.js Snippets Quick code generation for
getStaticProps,useSWR, etc.
Real Examples
Example 1: E-Commerce Product Listing with ISR
A product catalog with 10,000 items that updates inventory every hour.
js
// pages/products/[id].js
import { useRouter } from 'next/router';
export default function Product({ product }) {
const router = useRouter();
if (router.isFallback) { return
Loading...;
}
return (
{product.name}
${product.price}
Stock: {product.stock}
);
}
export async function getStaticPaths() {
const res = await fetch('https://api.example.com/products');
const products = await res.json();
const paths = products.map(product => ({
params: { id: product.id.toString() },
}));
return { paths, fallback: 'blocking' };
}
export async function getStaticProps({ params }) {
const res = await fetch(https://api.example.com/products/${params.id});
const product = await res.json();
return {
props: { product },
revalidate: 3600, // Regenerate every hour
};
}
This approach ensures fast load times for all products while keeping inventory data fresh.
Example 2: User Dashboard with getServerSideProps
A dashboard showing real-time analytics for logged-in users.
js
// pages/dashboard.js
import { useEffect } from 'react';
export default function Dashboard({ user, stats }) {
return (
Welcome, {user.name}
Total Revenue: ${stats.revenue}
Active Users: {stats.users}
);
}
export async function getServerSideProps(context) {
const { req } = context;
const token = req.headers.cookie?.match(/token=([^;]+)/)?.[1];
if (!token) {
return { redirect: { destination: '/login', permanent: false } };
}
const [userRes, statsRes] = await Promise.all([
fetch('https://api.example.com/user', { headers: { Authorization: Bearer ${token} } }),
fetch('https://api.example.com/analytics', { headers: { Authorization: Bearer ${token} } }),
]);
const user = await userRes.json();
const stats = await statsRes.json();
return { props: { user, stats } };
}
This ensures only authenticated users can access the dashboard and always sees live data.
Example 3: Search with SWR and API Routes
A search interface that fetches results as the user types.
js
// pages/search.js
import { useState } from 'react';
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then((r) => r.json());
export default function Search() {
const [query, setQuery] = useState('');
const { data, error } = useSWR(
query ? /api/search?q=${encodeURIComponent(query)} : null,
fetcher
);
return (
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/> {error &&
Error: {error.message}} {data?.length === 0 &&
No results found.}
{data && (
- {item.title}
{data.map(item => (
))}
)}
);
}
API route at pages/api/search.js returns filtered results based on the query string.
FAQs
Whats the difference between getStaticProps and getServerSideProps?
getStaticProps runs at build time and generates static HTML. getServerSideProps runs on every request and renders the page dynamically on the server. Use static for content that doesnt change often; use server-side for personalized or frequently changing data.
Can I use both getStaticProps and getServerSideProps in the same page?
No. A page can only export one of them. Choose based on your datas update frequency and user requirements.
Is client-side data fetching bad for SEO?
Yes, if its used for primary content. Search engines may not wait for JavaScript to execute before indexing. Always prefer server-side or static rendering for content that should appear in search results.
How do I handle authentication with getStaticProps?
You cant. getStaticProps runs at build time, before any user session exists. Use getServerSideProps or client-side fetching with cookies/tokens for authenticated content.
Can I use getStaticProps with dynamic routes?
Yes. Combine it with getStaticPaths to generate static pages for each dynamic route (e.g., /blog/[slug]).
What is Incremental Static Regeneration (ISR)?
ISR allows you to update static pages after build without rebuilding the entire site. You specify a revalidate time (in seconds), and Next.js will regenerate the page in the background on the first request after that interval.
Should I use Axios or fetch in Next.js?
For most cases, use the native fetch APIits built into Node.js and the browser, and works seamlessly with Next.js. Use Axios only if you need advanced features like interceptors or request cancellation.
How do I test data fetching in Next.js?
Use tools like MSW (Mock Service Worker) to intercept API calls during development. You can also mock responses in getStaticProps or getServerSideProps using local JSON files.
Does data fetching affect Next.js performance?
Yes, but strategically. Static generation and ISR offer the best performance. Server-side rendering adds slight latency per request. Client-side fetching can cause layout shifts or delays if not handled properly. Always prioritize server-side rendering for critical content.
Conclusion
Data fetching in Next.js is not a one-size-fits-all taskits a strategic decision that impacts performance, SEO, and user experience. By mastering getStaticProps, getServerSideProps, and client-side libraries like SWR, you gain the flexibility to build applications that are fast, scalable, and maintainable. The key is understanding when to use each method and aligning your approach with your contents nature: static, dynamic, or interactive.
Start with static generation whenever possible. Use server-side rendering for user-specific or real-time data. Reserve client-side fetching for post-load interactions. Combine these techniques with TypeScript, environment variables, and performance monitoring tools to build production-ready applications that rank well, load instantly, and delight users.
Next.js continues to evolve, and so should your data-fetching strategies. Stay updated with the latest releases, experiment with ISR, and always measure your results using Lighthouse and Web Vitals. The future of web development belongs to applications that are not just functionalbut fast, accessible, and optimized from the ground up. Mastering data fetching in Next.js is your first step toward building them.