Complete Next.js data fetching system. 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 static generation, (7) Revalidation strategies, (8) Error handling for data. Provides: Fetch patterns, caching options, streaming UI, API route handlers, client fetching setup. Ensures optimal data loading with proper caching and error handling.
/plugin marketplace add JosiahSiegel/claude-plugin-marketplace/plugin install nextjs-master@claude-plugin-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
| Pattern | Code | Purpose |
|---|---|---|
| Server fetch | await 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 |
| Config | Value | Effect |
|---|---|---|
dynamic | 'force-dynamic' | Always SSR |
revalidate | 60 | ISR every 60s |
fetchCache | 'force-no-store' | No caching |
| Client Library | Hook | Use Case |
|---|---|---|
| SWR | useSWR(key, fetcher) | Simple client fetching |
| TanStack Query | useQuery({ queryKey, queryFn }) | Complex state/mutations |
Use for data loading patterns:
Related skills:
nextjs-cachingnextjs-server-actionsnextjs-app-router// 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>
);
}
// 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>
);
}
// 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>
);
}
// 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>
);
}
// 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'] },
});
// 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
// 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');
}
// 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>
);
}
// 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>
);
}
'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>
);
}
// 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>
);
}
// 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 });
}
// 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 });
}
// 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);
}
// 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;
}
// 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>;
}
// Allow dynamic paths beyond generateStaticParams
export const dynamicParams = true; // default
// 404 for paths not in generateStaticParams
export const dynamicParams = false;
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>;
}
}
// 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>
);
}
| Practice | Description |
|---|---|
| Fetch in Server Components | Avoid client-side fetching when possible |
| Use parallel fetching | Promise.all for independent data |
| Implement streaming | Suspense for progressive loading |
| Cache appropriately | Use revalidate or tags for fresh data |
| Handle errors | Use error.tsx boundaries |
| Type your data | Use TypeScript for API responses |
Use when working with Payload CMS projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.
Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.