mohamed-hossam1/nextjs-skills11 files

Nextjs Cache Architecture

Use this skill whenever the user wants to design or implement caching in a Next.js 16+ App Router project — setting up the "use cache" directive, building a cache tag registry, wiring mutations to invalidation utilities, structuring Suspense boundaries for partial prerendering, handling personalized content near cache boundaries, choosing cacheLife profiles, calling cacheTag / updateTag / revalidateTag correctly, migrating from unstable_cache, or debugging stale or incorrectly fresh data. Trigger even when the user only describes their domain (e.g. "I have a posts table") and asks how to cache it properly.

Specification
Skill ID
mohamed-hossam1/nextjs-skills/nextjs-cache-architecture
Publisher
mohamed-hossam1
Repository
nextjs-skills
Installs
0
Files
11
Synced
Sep 23, 2026
How to use it

Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.

mohamed-hossam1/nextjs-skills/nextjs-cache-architectureInstalls these files
  • SKILL.md
  • assets/SuspenseOnSearchParams.tsx
  • assets/revalidate.ts
  • assets/tags.ts
  • evals/evals.json
  • overlay.yaml
  • references/core-concepts.md
  • references/debugging-and-checklist.md
  • references/migration-from-unstable-cache.md
  • references/personalized-content.md
  • scripts/audit.mjs

What this skill tells the agent

Next.js Cache Architecture

Architect caching in a Next.js 16+ App Router project from day one — not just dropping "use cache" where it happens to fit, but structuring the tag registry, revalidation utilities, Suspense boundaries, and mutation wiring so the cache stays correct as the codebase grows.

How to use this skill

Apply every rule and template below to the user's actual project. Replace placeholders like [Entity] and [collection] with names from their codebase before writing any code.

$ARGUMENTS

Where to look next

Most implementations only need this file. Load a reference when the task calls for it.

If the user is...Read
Asking how cache keys are derived, what cacheLife profiles mean, or hitting a "use cache" limitationreferences/core-concepts.md
Caching anything that depends on a logged-in userreferences/personalized-content.md
Reporting stale data, or doing a final review passreferences/debugging-and-checklist.md
Migrating an existing codebase off unstable_cachereferences/migration-from-unstable-cache.md

Drop-in templates in assets/ (rename placeholders to match the user's codebase):

  • assets/tags.ts → lib/cache/tags.ts
  • assets/revalidate.ts → lib/cache/revalidate.ts
  • assets/SuspenseOnSearchParams.tsx → components/SuspenseOnSearchParams.tsx

The architecture in one breath

A correct cache implementation has three load-bearing pieces. Build all three on day one — adding them later is much harder than getting them right up front.

  1. Tag registry (lib/cache/tags.ts) — every tag string lives here. No raw strings anywhere else.
  2. Revalidation utilities (lib/cache/revalidate.ts) — every updateTag() lives here. Mutations import from this file.
  3. Cache placement on data, not on pages — "use cache" goes on data-fetching functions or cached child components. Page components orchestrate Suspense boundaries; the children fetch.

Once those three are in place, the rest is just applying them consistently.

Step 1 — Enable Cache Components

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  cacheComponents: true,
};

export default nextConfig;

Step 2 — Build the cache tag registry

File: lib/cache/tags.ts (template: assets/tags.ts)

Use the assets/tags.ts template. The as const satisfies TagRegistry shape gives literal types and rejects malformed entries at compile time.

// lib/cache/tags.ts (skeleton — full template in assets/tags.ts)

export const CACHE_TAGS = {
  // Collection tags — one per logical data group, always present.
  [collection]: "[collection]",

  // Entity tag factories — only when a mutation targets a single entry.
  [entity]: (id: string | number) => `[entity]:${id}`,
} as const;

Step 3 — Build revalidation utilities

File: lib/cache/revalidate.ts (template: assets/revalidate.ts)

All updateTag() calls live here. Mutations import these functions — they never call updateTag() directly.

// lib/cache/revalidate.ts
"use server";

import { updateTag } from "next/cache";
import { CACHE_TAGS } from "./tags";

function updateTags(tags: string[]) {
  for (const tag of tags) updateTag(tag);
}

// Bulk — any entry in the collection changed.
export async function revalidate[Collection]Cache() {
  updateTags([CACHE_TAGS.[collection]]);
}

// Surgical — one specific entry changed.
// Only write this if `CACHE_TAGS.[entity]` factory exists in the registry.
export async function revalidate[Entity]Cache(id: string | number) {
  updateTags([
    CACHE_TAGS.[collection], // always invalidate the parent collection too
    CACHE_TAGS.[entity](id),
  ]);
}

Step 4 — Implement data fetching

Place "use cache" in data-fetching functions. Never fetch inside page components — page components orchestrate, they do not fetch.

// lib/data/[domain].ts
import { cacheLife, cacheTag } from "next/cache";
import { CACHE_TAGS } from "@/lib/cache/tags";

const BASE_URL = process.env.API_BASE_URL!;

// Good: collection fetch.
export async function get[Collection]() {
  "use cache";
  cacheLife("hours");
  cacheTag(CACHE_TAGS.[collection]);

  const res = await fetch(`${BASE_URL}/[endpoint]`);
  return res.json();
}

// Good: entity fetch.
export async function get[Entity](id: string) {
  "use cache";
  cacheLife("hours");
  cacheTag(CACHE_TAGS.[collection]);
  // Add CACHE_TAGS.[entity](id) only if a mutation calls updateTag on this entry.

  const res = await fetch(`${BASE_URL}/[endpoint]/${id}`);
  return res.json();
}
// Bad: fetching in a page component bypasses caching and invalidation.
export default async function Page() {
  const res = await fetch("/api/items");
  const data = await res.json();
  return <View data={data} />;
}

Step 5 — Structure rendering boundaries

Every page follows this shape:

Page component (sync, orchestration only — no data fetching)
  ├── Static shell (layout, nav — no data)
  ├── <Suspense> → cached shared content
  └── <Suspense> → dynamic personalized content

Standard page

// app/[route]/page.tsx
import { Suspense } from "react";
import { cacheLife, cacheTag } from "next/cache";
import { CACHE_TAGS } from "@/lib/cache/tags";
import { get[Collection] } from "@/lib/data/[domain]";

export default function AnyPage() {
  return (
    <>
      <StaticShell />

      <Suspense fallback={<SharedSkeleton />}>
        <SharedContent />
      </Suspense>

      <Suspense fallback={<PersonalizedSkeleton />}>
        <PersonalizedSection />
      </Suspense>
    </>
  );
}

async function SharedContent() {
  "use cache";
  cacheLife("hours");
  cacheTag(CACHE_TAGS.[collection]);

  const data = await get[Collection]();
  return <[Collection]List data={data} />;
}

Dynamic route page

// app/[domain]/[id]/page.tsx
import { Suspense } from "react";
import { cacheLife, cacheTag } from "next/cache";
import { CACHE_TAGS } from "@/lib/cache/tags";
import { get[Entity] } from "@/lib/data/[domain]";

export default function EntityPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  return (
    <Suspense fallback={<EntitySkeleton />}>
      <EntityDetail params={params} />
    </Suspense>
  );
}

async function EntityDetail({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  return <CachedEntityView id={id} />;
}

async function CachedEntityView({ id }: { id: string }) {
  "use cache";
  cacheLife("hours");
  cacheTag(CACHE_TAGS.[collection]);
  // Add CACHE_TAGS.[entity](id) only if a mutation needs surgical invalidation.

  const item = await get[Entity](id);
  return <[Entity]View item={item} />;
}

Filtered / search params page

// app/[route]/page.tsx
import { cacheLife, cacheTag } from "next/cache";
import { CACHE_TAGS } from "@/lib/cache/tags";
import { get[Collection]ByFilter } from "@/lib/data/[domain]";
import SuspenseOnSearchParams from "@/components/SuspenseOnSearchParams";

export default function FilteredPage({
  searchParams,
}: {
  searchParams: Promise<Record<string, string>>;
}) {
  return (
    <SuspenseOnSearchParams fallback={<FilteredListSkeleton />}>
      <FilteredList searchParams={searchParams} />
    </SuspenseOnSearchParams>
  );
}

async function FilteredList({
  searchParams,
}: {
  searchParams: Promise<Record<string, string>>;
}) {
  "use cache";
  cacheLife("minutes");

Use this skill in a project

Open any RiverX project, search the Skills panel for mohamed-hossam1/nextjs-skills/nextjs-cache-architecture, and install it in one click.

Use this skill