Skip to content
nextjs-data-fetching logo

Next.js Data Fetching

nextjs-data-fetching

Complete Next.js data fetching system (Next.js 15.5/16). PROACTIVELY activate for: (1) Server Component data fetching, (2) Parallel and sequential fetching, (3) Streaming with Suspense, (4) Route Handlers (API routes), (5) Client-side fetching with SWR/TanStack Query, (6) generateStaticParams for...

SKILL.md

Full skill instructions

Quick Reference

PatternCodePurpose
Server fetchawait fetch(url)Cached by default
No cache{ cache: 'no-store' }Always fresh
Revalidate{ next: { revalidate: 60 } }Time-based refresh
Tags{ next: { tags: ['posts'] } }Tag-based invalidation
ConfigValueEffect
dynamic'force-dynamic'Always SSR
revalidate60ISR every 60s
fetchCache'force-no-store'No caching
Client LibraryHookUse Case
SWRuseSWR(key, fetcher)Simple client fetching
TanStack QueryuseQuery({ queryKey, queryFn })Complex state/​mutations

When to Use This Skill

Use for data loading patterns:

  • Fetching data in Server Components
  • Setting up parallel data fetching with Promise.all
  • Implementing streaming with Suspense boundaries
  • Creating API routes with Route Handlers
  • Client-side data fetching and mutations

Related skills:

  • For caching strategies: see nextjs-caching
  • For Server Actions (mutations): see nextjs-server-actions
  • For App Router basics: see nextjs-app-router

Next.js Data Fetching

Server Components Data Fetching

Basic Data Fetching

// app/​posts/​page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/​posts');
  if (!res.ok) throw new Error('Failed to fetch posts');
  return res.json();
}

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

  return (
    <ul>
      {posts.map((post: Post) => (
        <li key={post.id}>{post.title}</​li>
      ))}
    </​ul>
  );
}

Direct Database Access

// app/​users/​page.tsx
import { db } from '@/​lib/​db';

export default async function UsersPage() {
  // Direct database query - no API needed
  const users = await db.users.findMany({
    orderBy: { createdAt: 'desc' },
    take: 10,
  });

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</​li>
      ))}
    </​ul>
  );
}

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() {
  // Fetch all data in parallel
  const [user, posts, analytics] = await Promise.all([
    getUser(),
    getPosts(),
    getAnalytics(),
  ]);

  return (
    <div>
      <UserCard user={user} />
      <PostsList posts={posts} />
      <AnalyticsChart data={analytics} />
    </​div>
  );
}

Sequential Data Fetching

// When data depends on previous request
async function getUser(userId: string) {
  const res = await fetch(`https://api.example.com/​users/​${userId}`);
  return res.json();
}

async function getUserPosts(userId: string) {
  const res = await fetch(`https://api.example.com/​users/​${userId}/​posts`);
  return res.json();
}

export default async function UserPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;

  // Sequential: user first, then posts
  const user = await getUser(id);
  const posts = await getUserPosts(user.id);

  return (
    <div>
      <h1>{user.name}</​h1>
      <PostsList posts={posts} />
    </​div>
  );
}

Caching and Revalidation

Caching Behavior

// Cached by default (equivalent to cache: 'force-cache')
const data = await fetch('https://api.example.com/​data');

// Opt out of caching
const data = await fetch('https://api.example.com/​data', {
  cache: 'no-store',
});

// Time-based revalidation
const data = await fetch('https://api.example.com/​data', {
  next: { revalidate: 3600 }, // Revalidate every hour
});

// Tag-based revalidation
const data = await fetch('https://api.example.com/​posts', {
  next: { tags: ['posts'] },
});

Route Segment Config

// app/​posts/​page.tsx
export const dynamic = 'force-dynamic'; // Always dynamic
// or
export const revalidate = 60; // Revalidate every 60 seconds
// or
export const fetchCache = 'force-no-store'; // Don't cache any fetches

Revalidating Data

// app/​actions.ts
'use server';

import { revalidatePath, revalidateTag } from 'next/​cache';

export async function createPost(formData: FormData) {
  await db.posts.create({
    data: { title: formData.get('title') as string },
  });

  // Revalidate specific path
  revalidatePath('/​posts');

  // Or revalidate by tag
  revalidateTag('posts');
}

Streaming with Suspense

Component-Level Streaming

// app/​dashboard/​page.tsx
import { Suspense } from 'react';

async function SlowComponent() {
  const data = await fetch('https://api.example.com/​slow-data');
  return <div>{/​* render data */​}</​div>;
}

async function FastComponent() {
  const data = await fetch('https://api.example.com/​fast-data');
  return <div>{/​* render data */​}</​div>;
}

export default function DashboardPage() {
  return (
    <div>
      {/​* Fast component renders immediately */​}
      <Suspense fallback={<FastSkeleton />}>
        <FastComponent />
      </​Suspense>

      {/​* Slow component streams in when ready */​}
      <Suspense fallback={<SlowSkeleton />}>
        <SlowComponent />
      </​Suspense>
    </​div>
  );
}

Nested Suspense Boundaries

// app/​page.tsx
import { Suspense } from 'react';

export default function Page() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <Header />
      <Suspense fallback={<SidebarSkeleton />}>
        <Sidebar />
      </​Suspense>
      <Suspense fallback={<ContentSkeleton />}>
        <MainContent />
        <Suspense fallback={<CommentsSkeleton />}>
          <Comments />
        </​Suspense>
      </​Suspense>
    </​Suspense>
  );
}

Client-Side Data Fetching

SWR

'use client';

import useSWR from 'swr';

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

export function UserProfile({ userId }: { userId: string }) {
  const { data, error, isLoading, mutate } = useSWR(
    `/​api/​users/​${userId}`,
    fetcher
  );

  if (isLoading) return <div>Loading...</​div>;
  if (error) return <div>Error loading user</​div>;

  return (
    <div>
      <h1>{data.name}</​h1>
      <button onClick={() => mutate()}>Refresh</​button>
    </​div>
  );
}

TanStack Query

// providers/​query-provider.tsx
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/​react-query';
import { useState } from 'react';

export function QueryProvider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: 60 * 1000,
          },
        },
      })
  );

  return (
    <QueryClientProvider client={queryClient}>{children}</​QueryClientProvider>
  );
}
// components/​Posts.tsx
'use client';

import { useQuery, useMutation, useQueryClient } from '@tanstack/​react-query';

async function fetchPosts() {
  const res = await fetch('/​api/​posts');
  return res.json();
}

async function createPost(data: { title: string }) {
  const res = await fetch('/​api/​posts', {
    method: 'POST',
    body: JSON.stringify(data),
  });
  return res.json();
}

export function Posts() {
  const queryClient = useQueryClient();

  const { data: posts, isLoading } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  });

  const mutation = useMutation({
    mutationFn: createPost,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
  });

  if (isLoading) return <div>Loading...</​div>;

  return (
    <div>
      <ul>
        {posts?.map((post: Post) => (
          <li key={post.id}>{post.title}</​li>
        ))}
      </​ul>
      <button onClick={() => mutation.mutate({ title: 'New Post' })}>
        Add Post
      </​button>
    </​div>
  );
}

Route Handlers (API Routes)

Basic Route Handler

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

export async function GET() {
  const posts = await db.posts.findMany();
  return NextResponse.json(posts);
}

export async function POST(request: Request) {
  const body = await request.json();
  const post = await db.posts.create({ data: body });
  return NextResponse.json(post, { status: 201 });
}

Dynamic Route Handler

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

interface RouteParams {
  params: Promise<{ id: string }>;
}

export async function GET(request: Request, { params }: RouteParams) {
  const { id } = await params;
  const post = await db.posts.findUnique({ where: { id } });

  if (!post) {
    return NextResponse.json({ error: 'Not found' }, { status: 404 });
  }

  return NextResponse.json(post);
}

export async function PUT(request: Request, { params }: RouteParams) {
  const { id } = await params;
  const body = await request.json();
  const post = await db.posts.update({
    where: { id },
    data: body,
  });
  return NextResponse.json(post);
}

export async function DELETE(request: Request, { params }: RouteParams) {
  const { id } = await params;
  await db.posts.delete({ where: { id } });
  return new NextResponse(null, { status: 204 });
}

Request Helpers

// app/​api/​search/​route.ts
import { NextResponse } from 'next/​server';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const query = searchParams.get('q');
  const page = parseInt(searchParams.get('page') || '1');

  const results = await search(query, page);

  return NextResponse.json(results);
}

Headers and Cookies

// app/​api/​auth/​route.ts
import { NextResponse } from 'next/​server';
import { cookies, headers } from 'next/​headers';

export async function GET() {
  const cookieStore = await cookies();
  const token = cookieStore.get('token');

  const headersList = await headers();
  const authorization = headersList.get('authorization');

  // Set cookie in response
  const response = NextResponse.json({ success: true });
  response.cookies.set('session', 'value', {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 60 * 60 * 24, // 1 day
  });

  return response;
}

Static Generation

generateStaticParams

// app/​posts/[slug]/​page.tsx
export async function generateStaticParams() {
  const posts = await db.posts.findMany({ select: { slug: true } });

  return posts.map((post) => ({
    slug: post.slug,
  }));
}

export default async function PostPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await db.posts.findUnique({ where: { slug } });
  return <article>{post?.content}</​article>;
}

Dynamic Params Behavior

// Allow dynamic paths beyond generateStaticParams
export const dynamicParams = true; // default

// 404 for paths not in generateStaticParams
export const dynamicParams = false;

Error Handling

Fetch Error Handling

async function getData() {
  const res = await fetch('https://api.example.com/​data');

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

  return res.json();
}

export default async function Page() {
  try {
    const data = await getData();
    return <div>{/​* render data */​}</​div>;
  } catch (error) {
    return <div>Error loading data</​div>;
  }
}

Using error.tsx

// app/​posts/​error.tsx
'use client';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div>
      <h2>Error loading posts</​h2>
      <p>{error.message}</​p>
      <button onClick={() => reset()}>Try again</​button>
    </​div>
  );
}

Best Practices

PracticeDescription
Fetch in Server ComponentsAvoid client-side fetching when possible
Use parallel fetchingPromise.all for independent data
Implement streamingSuspense for progressive loading
Cache appropriatelyUse revalidate or tags for fresh data
Handle errorsUse error.tsx boundaries
Type your dataUse TypeScript for API responses

More skills from JosiahSiegel

debugging-troubleshooting-2025 logo
JosiahSiegel/claude-plugin-marketplace

debugging-troubleshooting-2025

Comprehensive bash script debugging and troubleshooting techniques for 2025. PROACTIVELY activate for: (1) debugging a misbehaving bash script, (2) using set -x, set -e, set -u, set -o pipefail (strict mode), (3) customizing PS4 for richer trace output, (4) trap DEBUG, ERR, and EXIT for diagnosti...

58 0
View
tailwindcss-accessibility logo
JosiahSiegel/claude-plugin-marketplace

tailwindcss-accessibility

Tailwind CSS accessibility patterns including WCAG 2.2 compliance, touch targets, focus management, and ARIA. PROACTIVELY activate for: (1) building accessible UI with Tailwind, (2) WCAG 2.2 compliance (Level A, AA, AAA), (3) focus rings and focus-visible styling, (4) minimum touch-target sizes (...

58 0
View
sql-server-2025 logo
JosiahSiegel/claude-plugin-marketplace

sql-server-2025

SQL Server 2025 and SqlPackage 170.2.70 (October 2025) — vector databases, AI integration, and modern features. PROACTIVELY activate for: (1) SQL Server 2025 features, (2) vector type and vector indexes for AI, (3) Vector functions (VECTOR_DISTANCE, etc.), (4) AI integration (Azure OpenAI, embedd...

58 0
View
fal-api-reference logo
JosiahSiegel/claude-plugin-marketplace

fal-api-reference

Complete fal.ai API reference system. PROACTIVELY activate for: (1) @fal-ai/client JavaScript setup, (2) fal_client Python setup, (3) fal.subscribe/run/stream methods, (4) Queue management (submit/status/result), (5) Webhook configuration, (6) File upload to fal.media, (7) REST API endpoints, (8)...

58 0
View
powershell-security logo
JosiahSiegel/claude-plugin-marketplace

powershell-security

Modern PowerShell security practices for 2025 — SecretManagement, JEA, WDAC, credential protection. PROACTIVELY activate for: (1) Microsoft.PowerShell.SecretManagement and SecretStore for credential storage, (2) Just Enough Administration (JEA) endpoints, (3) Windows Defender Application Control ...

58 0
View
nextjs-middleware logo
JosiahSiegel/claude-plugin-marketplace

nextjs-middleware

Complete Next.js Middleware system (Next.js 15.5/16). PROACTIVELY activate for: (1) Creating middleware.ts (Edge) or proxy.ts (Node.js), (2) Matcher configuration, (3) Authentication protection, (4) Role-based access control, (5) Redirects and rewrites, (6) Internationalization (i18n), (7) Securi...

58 0
View
advanced-features-2025 logo
JosiahSiegel/claude-plugin-marketplace

advanced-features-2025

Advanced Claude Code plugin features for hooks, MCP integration, team distribution, and progressive disclosure. PROACTIVELY activate for: (1) progressive disclosure design, (2) hook automation, (3) PreToolUse and PostToolUse hooks, (4) MCP server integration, (5) .mcp.json configuration, (6) team...

58 0
View
azure-sql-optimization logo
JosiahSiegel/claude-plugin-marketplace

azure-sql-optimization

Azure SQL Database optimization and platform-specific features. PROACTIVELY activate for: (1) Azure SQL Database optimization, (2) DTU vs vCore selection and right-sizing, (3) automatic tuning (force last good plan, create/drop index), (4) Hyperscale tier and read replicas, (5) Serverless tier an...

58 0
View
github-actions-2025 logo
JosiahSiegel/claude-plugin-marketplace

github-actions-2025

GitHub Actions 2025-2026 features and modernization. PROACTIVELY activate for: (1) 1 vCPU Linux runners (October 2025 public preview), (2) immutable releases for hardened distribution, (3) Node24 migration from Node20, (4) reusable workflows and composite actions, (5) workflow_dispatch with rich ...

58 0
View
adf-master logo
JosiahSiegel/claude-plugin-marketplace

adf-master

Azure Data Factory (ADF) CI/CD, deployment, and pipeline development. PROACTIVELY activate for: (1) ADF CI/CD setup (npm validation, ARM template export), (2) ADF ARM template deployment, (3) ADF npm build validation in CI, (4) PrePostDeploymentScript for trigger and resource cleanup, (5) ADF Git...

58 0
View
git-2-49-features logo
JosiahSiegel/claude-plugin-marketplace

git-2-49-features

Git 2.49 specific features and performance improvements. PROACTIVELY activate for: (1) git-backfill command (new in 2.49) for partial clones, (2) path-walk API improvements, (3) zlib-ng integration for faster compression, (4) reftables backend stabilization, (5) revision-walk performance, (6) bun...

58 0
View
vitest-3-features logo
JosiahSiegel/claude-plugin-marketplace

vitest-3-features

Vitest 3 features and migration from Vitest 2 (2025). PROACTIVELY activate for: (1) Vitest 3 stable features, (2) workspace mode for monorepos, (3) test annotations and reporter API, (4) projects configuration, (5) browser mode in Vitest 3, (6) inline snapshots and toMatchInlineSnapshot, (7) type...

58 0
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View