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: fetch now defaults to no-store (dynamic). Explicitly set force-cache or revalidate to 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

SituationRecommendation
Static data rarely changedforce-cache + revalidateTag on mutations
User-specific datano-store or omit from SSG
Shared data fetched in multiple componentsWrap with cache() to deduplicate
Long-running queries on a page<Suspense> wrapping the slow component
Real-time / frequently updatingClient-side SWR / TanStack Query
Secret API keysServer Component / Route Handler only — never client
Cookies/headers in fetchUse headers() / cookies() from next/headers in server context

Reading Cookies and Headers in Fetch Context

import { cookies, headers } from 'next/headers'

export default async function Page() {
  const cookieStore = await cookies()
  const token = cookieStore.get('session')?.value

  const res = await fetch('https://api.example.com/me', {
    headers: { Authorization: `Bearer ${token}` },
    cache: 'no-store',
  })
  const user = await res.json()
  return <div>{user.name}</div>
}