Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
mohamed-hossam1 avatar

Nextjs Cache Architecture

  • 3.3k installs
  • 8 repo stars
  • Updated May 30, 2026
  • mohamed-hossam1/nextjs-cache-architecture

nextjs-cache-architecture is an agent skill that structures Next.js 16+ use cache tag registries, revalidation utilities, and Suspense-aware data fetching for correct invalidation.

About

nextjs-cache-architecture is an agent skill for designing correct caching in Next.js 16+ App Router projects from day one rather than sprinkling use cache ad hoc. It prescribes three load-bearing pieces: a centralized tag registry in lib/cache/tags.ts, revalidation utilities in lib/cache/revalidate.ts where every updateTag call lives, and cache placement on data-fetching functions instead of page components that only orchestrate Suspense. Stepwise guidance enables cacheComponents in next.config.ts, implements collection and entity tag factories with as const satisfies TagRegistry typing, wires mutations to revalidate helpers, and applies cacheLife plus cacheTag on getCollection and getEntity fetchers. Reference files cover core concepts, personalized content near cache boundaries, debugging checklists, and migration from unstable_cache, with drop-in assets for tags.ts, revalidate.ts, and SuspenseOnSearchParams.tsx. The skill triggers when users design cache tags, call cacheTag or updateTag, structure partial prerendering, or debug stale data after mutations. Developers reach for it when scaffolding SaaS apps that need deterministic invalidation as entity counts grow.

  • Three-piece architecture: tag registry, revalidation utilities, and data-layer use cache placement.
  • Enables cacheComponents in next.config.ts before implementing tags and fetch helpers.
  • assets/ templates provide tags.ts, revalidate.ts, and SuspenseOnSearchParams.tsx starters.
  • References cover personalized content, debugging checklists, and unstable_cache migration.
  • Mutations import revalidate helpers instead of calling updateTag with raw tag strings.

Nextjs Cache Architecture by the numbers

  • 3,265 all-time installs (skills.sh)
  • +110 installs in the week ending Aug 5, 2026 (Skillselion tracking)
  • Ranked #159 of 2,245 Frontend Development skills by installs in the Skillselion catalog
  • Security screen: LOW risk (skills.sh audit)
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
At a glance

nextjs-cache-architecture capabilities & compatibility

Capabilities
centralized cache tag registry design · mutation driven revalidation utilities · suspense boundary placement guidance · personalized content cache boundary patterns
Works with
vercel
Use cases
frontend · api development · ci cd
From the docs

What nextjs-cache-architecture says it does

every tag string lives here. No raw strings anywhere else.
SKILL.md
cacheComponents: true
SKILL.md
npx skills add https://github.com/mohamed-hossam1/nextjs-cache-architecture --skill nextjs-cache-architecture

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs3.3k
repo stars8
Security audit3 / 3 scanners passed
Last updatedMay 30, 2026
Repositorymohamed-hossam1/nextjs-cache-architecture

How do I design Next.js App Router caching so mutations invalidate the right tags without stale data or scattered raw tag strings?

Architect Next.js 16+ App Router caching with tag registries, revalidation utilities, Suspense boundaries, and correct use cache placement on data functions.

Who is it for?

Developers building Next.js 16+ SaaS apps who need centralized cache tags and mutation-driven revalidation from the start.

Skip if: Skip for Pages Router-only projects, client-only SPAs, or tasks unrelated to Next.js server caching.

When should I use this skill?

User sets up use cache, builds cache tag registries, wires updateTag after mutations, or debugs stale App Router data.

What you get

lib/cache/tags.ts registry, lib/cache/revalidate.ts helpers, and data functions with use cache, cacheLife, and cacheTag wired to mutations.

  • Cache tag registry
  • Revalidation utility module
  • Cached data fetch functions

Files

SKILL.mdMarkdownGitHub ↗

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.tslib/cache/tags.ts
  • assets/revalidate.tslib/cache/revalidate.ts
  • assets/SuspenseOnSearchParams.tsxcomponents/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");
  cacheTag(CACHE_TAGS.[collection]);
  // searchParams is an argument → auto-keyed per unique param combination.

  const { q = "", page = "1" } = await searchParams;
  return await get[Collection]ByFilter(q, page);
}

A standard <Suspense> does not re-trigger its fallback on client-side navigation when only searchParams changes. Use SuspenseOnSearchParams (template: assets/SuspenseOnSearchParams.tsx) on every page with search or filter params.

Step 6 — Handle personalized content

Read cookies() / headers() / auth() outside the cache boundary and pass the value as a prop. The argument becomes part of the auto-generated cache key, so each user gets their own entry. Calling any of those APIs inside a "use cache" function throws or produces wrong behavior.

See references/personalized-content.md for the full read-outside / cache-inside pattern and the rare "use cache: private" exception.

Step 7 — Wire mutations to invalidation

Mutations call revalidation utilities and never reach for updateTag() themselves. This keeps the cache layer mechanical and auditable from one file, and lets you add observability (logging, tracing) in one place.

// app/actions/[domain].ts
"use server";

import {
  revalidate[Collection]Cache,
  revalidate[Entity]Cache,
} from "@/lib/cache/revalidate";

export async function create[Entity](payload: unknown) {
  await db.[entity].create(payload);
  await revalidate[Collection]Cache();
}

export async function update[Entity](id: string | number, payload: unknown) {
  await db.[entity].update(id, payload);
  await revalidate[Entity]Cache(id); // requires the surgical utility to be exported
}

updateTag vs revalidateTag

Two APIs for two different needs:

APIEffectCall from
updateTag(tag)Immediate — the same request sees fresh dataServer actions, via revalidate.ts
revalidateTag(tag, "max")Background stale-while-revalidate — next request sees fresh dataRoute handlers, webhooks

revalidateTag always takes a second argument ("max" for stale-while-revalidate, { expire: 0 } for immediate hard expiry). The single-argument form is deprecated and silently does nothing in some configurations.

Common mistakes

When the cache misbehaves, walk these in order. The first six catch nearly everything; only run next build after the rest pass. The full debug walk and a sign-off checklist are in references/debugging-and-checklist.md.

Symptom or smellFix
Function runs uncached on every request"use cache" is after an await — move it to be the first statement.
Cached function throws or returns wrong data per userMove cookies() / headers() / auth() outside; pass values as arguments.
updateTag does nothingTag string typo, or no cacheTag ever registered the matching tag.
Mutation completes but the list still reads staleRevalidation utility called before the write, or not called at all.
Whole page re-renders even though only one section changedA dynamic child sits inside a cached parent — split with <Suspense>.
Filter UI doesn't show a loading state on navigationPlain <Suspense> — switch to SuspenseOnSearchParams.
Page marked dynamic when you expected staticRun next build; trace the leaked dynamic API in the route's source tree.
Page component fetches data directlyMove the fetch into a cached child; pages should orchestrate, not fetch.

For the full debug walk and a sign-off checklist, see references/debugging-and-checklist.md. To verify the static parts of a finished implementation against the user's project, run scripts/audit.mjs <project-root> — usage and what it checks are documented in README.md.

Related skills

Forks & variants (1)

Nextjs Cache Architecture has 1 known copy in the catalog totaling 2.2k installs. They canonicalize to this original listing.

How it compares

Pick nextjs-cache-architecture over ad-hoc updateTag calls when invalidation logic needs a single audit point and shared observability hooks.

FAQ

What are the three required cache architecture pieces?

A tag registry in lib/cache/tags.ts, revalidation utilities in lib/cache/revalidate.ts, and use cache on data functions not page components.

Where should updateTag be called?

Only inside lib/cache/revalidate.ts helpers that mutations import, never with raw tag strings scattered in route handlers.

Is Nextjs Cache Architecture safe to install?

skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.