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 site generation, and a robust data fetching ecosystem. 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
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 site generation, and a robust data fetching ecosystem. 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 CMS, querying a REST API, or interacting with a GraphQL endpoint, knowing the right methods and best practices ensures your application loads quickly, ranks well in search engines, and delivers a seamless user experience.
Unlike traditional React applications that rely primarily on client-side data fetching using useEffect and fetch or Axios, Next.js offers multiple data fetching strategies tailored to different use cases. These include Server Components with async/await, getServerSideProps, getStaticProps, and client-side fetching with React hooks. Each approach has distinct advantages depending on whether your data is static, dynamic, or user-specific.
This comprehensive guide will walk you through every method of fetching data in Next.js, from foundational concepts to advanced patterns. Youll learn when and why to use each technique, how to implement them correctly, and how to optimize for performance and SEO. By the end of this tutorial, youll have a clear, actionable framework for choosing the right data fetching strategy in any Next.js project whether you're building a blog, an e-commerce store, or a real-time dashboard.
Step-by-Step Guide
Understanding Data Fetching in Next.js
Before diving into implementation, its essential to understand the core philosophy behind data fetching in Next.js. Unlike client-side frameworks where data is typically fetched after the page loads, Next.js allows you to fetch data at build time or request time enabling you to generate HTML on the server before sending it to the browser. This leads to faster initial page loads, improved SEO, and better Core Web Vitals scores.
Next.js supports three primary data fetching methods:
- Server Components (with async/await) fetch data directly in components rendered on the server
- getServerSideProps fetch data on every request for dynamic pages
- getStaticProps fetch data at build time for static pages
- Client-side fetching use useEffect and fetch/Axios in React components
Each method serves a different purpose. Choosing the right one depends on how often your data changes, whether its user-specific, and whether SEO is a priority.
Method 1: Using Server Components with async/await
Introduced in Next.js 13 with the App Router, Server Components are the recommended way to fetch data in modern Next.js applications. They run exclusively on the server, allowing you to use async/await directly within your components without needing special functions like getServerSideProps.
Heres how to implement it:
First, create a new page in the app directory for example, app/posts/page.js:
jsx
// app/posts/page.js
import { useState } from 'react';
export default async function PostsPage() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await res.json();
return (
Latest Posts
{posts.map(post => (
{post.title}
{post.body}
))}
);
}
In this example, the fetch call runs on the server during the rendering phase. The resulting HTML is sent to the client fully rendered, meaning users see content immediately without waiting for JavaScript to load and execute.
Server Components also support caching by default. You can customize caching behavior using the next options:
jsx
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
next: { revalidate: 3600 } // Revalidate every hour
});
This enables Incremental Static Regeneration (ISR), where the page is regenerated in the background after the specified time, ensuring fresh content without sacrificing performance.
Method 2: Using getServerSideProps
If you're still using the Pages Router (pre-Next.js 13), or need to fetch data on every request for dynamic content, getServerSideProps is your go-to method. It runs on the server for every incoming request and passes the fetched data as props to your page component.
Create a page file in the pages directory for example, pages/user/[id].js:
jsx
// pages/user/[id].js
export async function getServerSideProps(context) {
const { id } = context.params;
const res = await fetch(https://jsonplaceholder.typicode.com/users/${id});
const user = await res.json();
return {
props: { user }
};
}
export default function UserPage({ user }) {
return (
{user.name}
Email: {user.email}
Website: {user.website}
);
}
In this example, the user data is fetched on every request. This is ideal for pages that display personalized or frequently changing data such as user profiles, dashboards, or shopping carts.
Important: getServerSideProps should not be used for static content. Since it runs on every request, it can lead to slower load times and increased server load compared to static generation.
Method 3: Using getStaticProps
getStaticProps is used to fetch data at build time. Its perfect for content that doesnt change often such as blog posts, product listings, or documentation pages. Pages generated with getStaticProps are pre-rendered into static HTML files, making them lightning-fast and SEO-friendly.
Heres an example using the Pages Router:
jsx
// pages/blog/[slug].js
export async function getStaticProps({ params }) {
const res = await fetch(https://jsonplaceholder.typicode.com/posts/${params.slug});
const post = await res.json();
return {
props: { post }
};
}
export async function getStaticPaths() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await res.json();
const paths = posts.map(post => ({
params: { slug: post.id.toString() }
}));
return {
paths,
fallback: 'blocking' // or true/false
};
}
export default function PostPage({ post }) {
return (
{post.title}
{post.body}
);
}
The getStaticPaths function tells Next.js which paths to generate at build time. The fallback option controls behavior for paths not generated at build time:
falsereturns a 404 for ungenerated pathstrueshows a loading state while generating the page on-demand'blocking'waits for generation before rendering (recommended for SEO)
For the App Router, you can achieve the same result using the generateStaticParams function:
jsx
// app/blog/[slug]/page.js
export async function generateStaticParams() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await res.json();
return posts.map(post => ({
slug: post.id.toString()
}));
}
export default async function PostPage({ params }) {
const res = await fetch(https://jsonplaceholder.typicode.com/posts/${params.slug});
const post = await res.json();
return (
{post.title}
{post.body}
);
}
Method 4: Client-Side Data Fetching
While server-side fetching is preferred for SEO and performance, there are cases where you need to fetch data after the page loads such as user interactions, form submissions, or real-time updates. For these scenarios, use client-side fetching with React hooks.
Heres an example using the useEffect hook and the native fetch API:
jsx
// app/dashboard/page.js
import { useState, useEffect } from 'react';
export default function Dashboard() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
const res = await fetch('/api/dashboard-data');
const result = await res.json();
setData(result);
setLoading(false);
};
fetchData();
}, []); if (loading) return
Loading...;
return (
Dashboard Data
{JSON.stringify(data, null, 2)}
);
}
You can also use libraries like React Query, SWR, or Axios for more advanced client-side data fetching with caching, polling, and error handling.
Example using SWR:
jsx
import useSWR from 'swr';
const fetcher = (...args) => fetch(...args).then(res => res.json());
export default function Dashboard() {
const { data, error, loading } = useSWR('/api/dashboard-data', fetcher); if (error) return
Error loading data; if (loading) return
Loading...;
return (
Dashboard Data
{JSON.stringify(data, null, 2)}
);
}
SWR automatically caches responses, retries failed requests, and supports background refetching making it ideal for dynamic UIs.
Integrating with External APIs
When fetching data from external APIs, always consider security and performance. Avoid exposing API keys in client-side code. Instead, create API routes in Next.js to proxy requests.
Create an API route at app/api/external-data/route.js:
jsx
// app/api/external-data/route.js
import { NextResponse } from 'next/server';
export async function GET() {
const response = await fetch('https://api.example.com/data', {
headers: {
'Authorization': Bearer ${process.env.API_KEY}
}
});
const data = await response.json();
return NextResponse.json(data);
}
Then fetch from your component:
jsx
const res = await fetch('/api/external-data');
const data = await res.json();
This keeps your API keys secure and allows you to handle authentication, rate limiting, and data transformation on the server.
Handling Errors and Loading States
Always implement proper error handling and loading indicators. For Server Components, use try/catch blocks:
jsx
export default async function PostsPage() {
let posts = [];
let error = null;
try {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', { next: { revalidate: 60 } });
if (!res.ok) throw new Error('Failed to fetch posts');
posts = await res.json();
} catch (err) {
error = err.message;
}
if (error) { return
Failed to load posts: {error};
}
return (
{posts.map(post => (
{post.title}
{post.body}
))}
);
}
For client-side fetching, use state to manage loading and error states as shown in earlier examples.
Best Practices
Choose the Right Data Fetching Strategy
The most common mistake developers make is using the wrong data fetching method for the use case. Heres a quick decision tree:
- Static content (blog, docs, product catalog) ? Use
getStaticPropsor Server Components withrevalidate - Dynamic, user-specific content (dashboards, profiles) ? Use
getServerSidePropsor Server Components - Interactive UIs (search, filters, real-time updates) ? Use client-side fetching with SWR or React Query
Never use client-side fetching for content that needs to be indexed by search engines. Google and other crawlers may not execute JavaScript immediately, leading to poor SEO.
Optimize Fetching with Caching and Revalidation
Next.js provides powerful caching mechanisms. Use the next option in Server Components to control caching:
revalidate: 3600regenerate every hour (ISR)next: { revalidate: 0 }disable caching (equivalent to SSR)next: { cache: 'force-cache' }cache indefinitely (use sparingly)
For API routes, use Cache-Control headers to influence CDN and browser caching:
jsx
return NextResponse.json(data, {
headers: {
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=59'
}
});
Use Environment Variables for API Keys
Never hardcode API keys in your components. Store them in a .env.local file:
NEXT_PUBLIC_API_URL=https://api.example.com
API_KEY=your-secret-key-here
Access them using process.env.API_KEY in server-side code. Prefix with NEXT_PUBLIC_ only if you need the value on the client side (rare).
Implement Loading Skeletons
Improve perceived performance by showing skeleton UIs while data loads. Libraries like react-loading-skeleton make this easy:
jsx
import Skeleton from 'react-loading-skeleton';
export default function PostList() {
const { data, error, isLoading } = useSWR('/api/posts', fetcher);
if (isLoading) {
return (
>
);
} if (error) return
Error;
return (
{data.map(post =>
);
}
Minimize Bundle Size with Code Splitting
Use dynamic imports for heavy client-side libraries:
jsx
import dynamic from 'next/dynamic';
const ChartComponent = dynamic(() => import('../components/Chart'), { loading: () =>
Loading chart...,
ssr: false // Don't render on server
});
This ensures heavy components dont block server rendering or increase initial payload.
Monitor Performance with Next.js Analytics
Next.js provides built-in performance metrics. Enable them in next.config.js:
js
module.exports = {
experimental: {
appDir: true
},
analytics: {
enabled: true
}
};
Use the next dev command to view performance reports in the terminal, including render times, data fetch durations, and bundle sizes.
Tools and Resources
Core Next.js Features
- App Router New routing system in Next.js 13+ with Server Components
- Pages Router Legacy routing system (still supported)
- API Routes Create backend endpoints within your Next.js app
- Middleware Run code before a request is completed (authentication, redirects)
Third-Party Libraries
- SWR React Hooks library for client-side data fetching by Vercel
- React Query Powerful state management for server state with caching, deduping, and polling
- Axios Popular HTTP client with interceptors and request/response handling
- GraphQL Request Lightweight GraphQL client for Next.js
- zod TypeScript-first schema validation for API responses
APIs for Testing
Use these free public APIs to test your data fetching implementations:
- JSONPlaceholder Fake REST API for testing
- PokeAPI Pokmon data API
- Dog API Random dog images
- Rick and Morty API Character and episode data
Development Tools
- Next.js DevTools Browser extension for inspecting data fetching and caching
- Postman Test API endpoints before integrating
- GraphQL Playground Explore and test GraphQL APIs
- VS Code Extensions ESLint, Prettier, and Next.js snippets
Documentation and Learning Resources
- Next.js Official Documentation
- SWR Documentation
- React Query Documentation
- Next.js YouTube Channel
- Next.js Learn Course Free interactive tutorials
Real Examples
Example 1: Blog with Static Generation
Lets build a blog that fetches posts from a CMS at build time and regenerates them hourly.
jsx
// app/blog/page.js
import { notFound } from 'next/navigation';
export default async function BlogPage() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
next: { revalidate: 3600 }
});
if (!res.ok) notFound();
const posts = await res.json();
return (
My Blog
{posts.slice(0, 5).map(post => (
{post.title}
{post.body.substring(0, 100)}...
))}
);
}
Example 2: E-Commerce Product Page with Server-Side Rendering
Fetch product details on every request to ensure pricing and stock are accurate.
jsx
// app/product/[id]/page.js
import { notFound } from 'next/navigation';
export default async function ProductPage({ params }) {
const res = await fetch(https://fakestoreapi.com/products/${params.id});
if (!res.ok) notFound();
const product = await res.json();
return (
{product.title}
${product.price}
{product.description}
);
}
Example 3: Dashboard with Real-Time Updates
Use SWR to poll for new notifications every 10 seconds.
jsx
// app/dashboard/page.js
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(res => res.json());
export default function Dashboard() {
const { data, error, isLoading } = useSWR('/api/notifications', fetcher, {
refreshInterval: 10000 // Poll every 10 seconds
}); if (isLoading) return
Loading...; if (error) return
Error: {error.message};
return (
Notifications
- {notification.message}
{data.map(notification => (
))}
);
}
Example 4: Secure API Proxy with Environment Variables
Fetch weather data securely using an API route.
app/api/weather/route.js
jsx
import { NextResponse } from 'next/server';
export async function GET() {
const apiKey = process.env.WEATHER_API_KEY;
const res = await fetch(https://api.openweathermap.org/data/2.5/weather?q=London&appid=${apiKey});
if (!res.ok) {
return NextResponse.json({ error: 'Failed to fetch weather' }, { status: 500 });
}
const data = await res.json();
return NextResponse.json(data);
}
app/weather/page.js
jsx
export default async function WeatherPage() {
const res = await fetch('/api/weather');
const weather = await res.json();
return (
Weather in London
Temperature: {Math.round(weather.main.temp - 273.15)}C
Condition: {weather.weather[0].description}
);
}
FAQs
Whats the difference between getStaticProps and getServerSideProps?
getStaticProps runs at build time and generates static HTML files. Its ideal for content that doesnt change often. getServerSideProps runs on every request and renders the page dynamically on the server. Use it for user-specific or frequently changing data.
Can I use both getStaticProps and getServerSideProps in the same page?
No. A page can only use one of them. Choose based on your datas volatility. For hybrid behavior, use Server Components with revalidate for ISR, or combine server-side fetching with client-side updates.
Is client-side data fetching bad for SEO?
Yes, if the content youre fetching is critical for search engines. Google can execute JavaScript, but it may take time to index content fetched after page load. For SEO-critical content, always prefer server-side rendering or static generation.
How do I handle authentication when fetching data in Next.js?
For server-side fetching, include authentication tokens in headers when calling external APIs via API routes. For client-side fetching, use cookies or tokens stored in memory or localStorage, and pass them in requests. Never store secrets in client-side code.
What is Incremental Static Regeneration (ISR)?
ISR allows you to update static pages after theyve been built. With revalidate, Next.js will regenerate the page in the background after a specified time, serving the old version until the new one is ready. This combines the speed of static sites with the freshness of dynamic ones.
Can I use GraphQL with Next.js?
Yes. Use libraries like graphql-request or apollo-client with Server Components or API routes. You can also use Next.js API routes to proxy GraphQL queries to your backend, keeping your API key secure.
How do I test data fetching in Next.js?
Use tools like Jest and React Testing Library to mock API responses. For API routes, use next-test-api-route-handler to simulate requests. Always test both successful and error cases.
Should I use Axios or fetch in Next.js?
Both work. fetch is built into Node.js and Next.js, so its the recommended choice for most cases. Axios offers more features like interceptors and automatic JSON parsing, but adds bundle size. Use fetch unless you need advanced features.
Conclusion
Mastering data fetching in Next.js is not just about learning syntax its about understanding the philosophy behind server-rendered applications and making informed decisions that impact performance, scalability, and SEO. Whether youre building a static blog, a dynamic dashboard, or a high-traffic e-commerce platform, the right data fetching strategy can make the difference between a slow, unindexed page and a lightning-fast, search-engine-optimized experience.
As Next.js continues to evolve, Server Components and the App Router represent the future of web development. Embrace these tools, prioritize server-side rendering for SEO-critical content, and use client-side fetching only when necessary. Combine these techniques with caching, error handling, and performance monitoring to build applications that are not only functional but also fast, reliable, and maintainable.
Start by auditing your current projects: identify which pages rely on client-side data fetching and consider migrating them to Server Components or static generation. Test the impact on Lighthouse scores and user engagement. Over time, youll develop an intuition for when to use each method and youll build applications that perform exceptionally well across all dimensions: speed, accessibility, and search visibility.
Remember: in the world of modern web development, the fastest page is not the one that loads JavaScript the quickest its the one that delivers meaningful content the moment the user clicks.