Next.js Cheatsheet
Data Fetching
Use this Next.js reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
Fetch in Server Components
The simplest pattern — async/await directly in a Server Component:
// app/posts/page.tsx export default async function PostsPage() { const res = await fetch('https://api.example.com/posts') const posts = await res.json() return ( <ul> {posts.map((p: Post) => <li key={p.id}>{p.title}</li>)} </ul> ) }
fetch Cache Options
Next.js extends the native fetch with caching semantics.
// Cache indefinitely (Static — default in Next 14) fetch('https://...', { cache: 'force-cache' }) // Never cache — always fresh (Dynamic) fetch('https://...', { cache: 'no-store' }) // Revalidate every N seconds (ISR-style) fetch('https://...', { next: { revalidate: 60 } }) // Tag for on-demand revalidation fetch('https://...', { next: { tags: ['posts'] } })
Next 15 change:
fetchnow defaults tono-store(dynamic). Explicitly setforce-cacheorrevalidateto opt into caching.
cache() — React Cache (Deduplication)
import { cache } from 'react' // Deduplicates identical calls within a single request export const getUser = cache(async (id: string) => { return db.users.findUnique({ where: { id } }) }) // Both calls below hit the DB only once per request const user1 = await getUser('42') const user2 = await getUser('42') // returns cached result
Use cache() for non-fetch data sources (DB, ORM, file system).
unstable_cache — Persistent Cache
import { unstable_cache } from 'next/cache' const getCachedUser = unstable_cache( async (id: string) => db.users.findUnique({ where: { id } }), ['user'], // cache key parts { revalidate: 3600, // seconds tags: ['users'], // for on-demand invalidation } ) const user = await getCachedUser('42')
revalidatePath and revalidateTag
Trigger on-demand cache invalidation from Server Actions or Route Handlers:
import { revalidatePath, revalidateTag } from 'next/cache' // Invalidate all data for a specific path revalidatePath('/blog') revalidatePath('/blog/[slug]', 'page') // scoped to page revalidatePath('/dashboard', 'layout') // scoped to layout // Invalidate all fetches tagged with 'posts' revalidateTag('posts')
Parallel Data Fetching
Avoid waterfall fetches — run independent queries concurrently with Promise.all:
export default async function Dashboard() { // Sequential (slow — each waits for the previous) // const user = await getUser() // const posts = await getPosts() // Parallel (fast — both fire at once) const [user, posts] = await Promise.all([ getUser(), getPosts(), ]) return <div>...</div> }
Sequential (Waterfall) When Needed
export default async function Profile({ params }: { params: Promise<{ id: string }> }) { const { id } = await params const user = await getUser(id) // Only fetch posts after we have the user const posts = await getPostsByUser(user.id) return <div>...</div> }
Streaming with <Suspense>
Show partial UI immediately; stream slower data as it resolves.
// app/dashboard/page.tsx import { Suspense } from 'react' import { UserProfile } from '@/components/user-profile' import { SlowFeed } from '@/components/slow-feed' export default function Dashboard() { return ( <div> <Suspense fallback={<p>Loading profile…</p>}> <UserProfile /> </Suspense> <Suspense fallback={<p>Loading feed…</p>}> <SlowFeed /> </Suspense> </div> ) }
// components/slow-feed.tsx (Server Component) async function SlowFeed() { const data = await slowQuery() // this causes the suspense return <ul>{data.map(…)}</ul> }
Client-Side Fetching
For data that must be client-side (user-specific after hydration, real-time):
With useEffect:
'use client' import { useState, useEffect } from 'react' export function LivePrice({ symbol }: { symbol: string }) { const [price, setPrice] = useState<number | null>(null) useEffect(() => { fetch(`/api/price?symbol=${symbol}`) .then(r => r.json()) .then(d => setPrice(d.price)) }, [symbol]) return <span>{price ?? '…'}</span> }
With SWR (recommended):
npm install swr
'use client' import useSWR from 'swr' const fetcher = (url: string) => fetch(url).then(r => r.json()) export function UserCard({ id }: { id: string }) { const { data, error, isLoading } = useSWR(`/api/users/${id}`, fetcher) if (isLoading) return <p>Loading…</p> if (error) return <p>Error</p> return <div>{data.name}</div> }
With TanStack Query:
'use client' import { useQuery } from '@tanstack/react-query' export function Posts() { const { data, isPending } = useQuery({ queryKey: ['posts'], queryFn: () => fetch('/api/posts').then(r => r.json()), }) if (isPending) return <p>Loading…</p> return <ul>{data.map((p: Post) => <li key={p.id}>{p.title}</li>)}</ul> }
Data Fetching in Route Handlers
// app/api/posts/route.ts import { NextResponse } from 'next/server' export async function GET() { const posts = await db.posts.findMany() return NextResponse.json(posts) }
Gotchas and Best Practices
| Situation | Recommendation |
|---|---|
| Static data rarely changed | force-cache + revalidateTag on mutations |
| User-specific data | no-store or omit from SSG |
| Shared data fetched in multiple components | Wrap with cache() to deduplicate |
| Long-running queries on a page | <Suspense> wrapping the slow component |
| Real-time / frequently updating | Client-side SWR / TanStack Query |
| Secret API keys | Server Component / Route Handler only — never client |
| Cookies/headers in fetch | Use headers() / cookies() from next/headers in server context |