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

Nov 10, 2025 - 08:37
Nov 10, 2025 - 08:37
 3

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:

  • false returns a 404 for ungenerated paths
  • true shows 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 getStaticProps or Server Components with revalidate
  • Dynamic, user-specific content (dashboards, profiles) ? Use getServerSideProps or 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: 3600 regenerate 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:

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

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)}...

Read more

))}

);

}

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

    {data.map(notification => (

  • {notification.message}
  • ))}

);

}

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.