Data fetching + caching cheatsheet.

fetch in server components

async function Page() {
  const data = await fetch("https://api.example.com/users").then(r => r.json());
  return <ul>{data.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}

By default Next caches successful GET responses.

fetch options

fetch(url, { cache: "force-cache" });    // memo + persisted (default in builds)
fetch(url, { cache: "no-store" });       // always fresh
fetch(url, { next: { revalidate: 60 } }); // ISR
fetch(url, { next: { tags: ["users"] } }); // tag-based

ISR per route

export const revalidate = 60;

Applied to all fetch calls in this route without explicit override.

revalidatePath / revalidateTag

"use server";
import { revalidatePath, revalidateTag } from "next/cache";

export async function createPost(formData: FormData) {
  await db.post.create({ ... });
  revalidatePath("/posts");
  // OR
  revalidateTag("users");
}

Triggers re-render of cached pages on demand.

Parallel data fetching

async function Page() {
  const [user, posts] = await Promise.all([
    fetchUser(),
    fetchPosts(),
  ]);
  return <View user={user} posts={posts} />;
}

Sequential awaits would block — Promise.all is much faster.

Streaming with Suspense

import { Suspense } from "react";

async function User() {
  const u = await fetchUser();   // slow
  return <p>{u.name}</p>;
}

async function Posts() {
  const ps = await fetchPosts();  // slow
  return <ul>{ps.map(...)}</ul>;
}

export default function Page() {
  return (
    <>
      <h1>Profile</h1>
      <Suspense fallback={<p>loading user</p>}>
        <User />
      </Suspense>
      <Suspense fallback={<p>loading posts</p>}>
        <Posts />
      </Suspense>
    </>
  );
}

User and Posts stream in independently.

cache() — request-scoped memoization

import { cache } from "react";

export const getUser = cache(async (id: number) => {
  return db.user.find(id);
});

Within one request, multiple getUser(1) calls fetch once.

unstable_cache — persisted cache

import { unstable_cache } from "next/cache";

export const getTopPosts = unstable_cache(
  async () => db.post.findMany({ orderBy: { views: "desc" }, take: 10 }),
  ["top-posts"],
  { revalidate: 300, tags: ["posts"] },
);

Persists across requests.

No-cache patterns

import { cookies, headers } from "next/headers";

async function Page() {
  cookies();                 // forces dynamic rendering
  return <p>...</p>;
}

Using cookies/headers opts into dynamic rendering.

Reading cookies

import { cookies } from "next/headers";

async function Page() {
  const c = await cookies();
  const session = c.get("session")?.value;
  ...
}

Setting cookies (server action / route handler)

"use server";
import { cookies } from "next/headers";

export async function login(formData: FormData) {
  const c = await cookies();
  c.set("session", "...", { httpOnly: true, secure: true, sameSite: "lax" });
}

Reading headers

import { headers } from "next/headers";

async function Page() {
  const h = await headers();
  const ua = h.get("user-agent");
}

With TanStack Query (client)

"use client";
import { useQuery } from "@tanstack/react-query";

export function Users() {
  const { data } = useQuery({
    queryKey: ["users"],
    queryFn: () => fetch("/api/users").then(r => r.json()),
  });
  return <ul>{data?.map(...)}</ul>;
}

Hydrate from server:

// page.tsx (server)
const qc = new QueryClient();
await qc.prefetchQuery({ queryKey: ["users"], queryFn: fetchUsers });

return (
  <HydrationBoundary state={dehydrate(qc)}>
    <Users />
  </HydrationBoundary>
);

Database queries directly

import { db } from "@/lib/db";

async function Page() {
  const users = await db.user.findMany({ take: 10 });
  return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}

No HTTP roundtrip — direct DB access from server components.

Auth-gated data

import { redirect } from "next/navigation";
import { getSession } from "@/lib/auth";

async function Page() {
  const session = await getSession();
  if (!session) redirect("/login");
  
  const user = await db.user.find(session.userId);
  return <Profile user={user} />;
}

Common mistakes

  • Sequential awaits instead of Promise.all.
  • Forgetting revalidate after mutations → stale data.
  • Using unstable_cache with values that change per user.
  • force-dynamic everywhere → loses caching benefits.
  • fetch with Authorization header without cache: "no-store" — caches per-user data.

Read this next

If you want my data fetching + cache patterns, they’re at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .