{"id":17172237,"url":"https://github.com/lucasconstantino/next-cache-tags","last_synced_at":"2025-04-13T16:20:56.307Z","repository":{"id":63592262,"uuid":"568577437","full_name":"lucasconstantino/next-cache-tags","owner":"lucasconstantino","description":"Active ISR revalidation based on surrogate keys for Next.js","archived":false,"fork":false,"pushed_at":"2023-08-04T13:49:30.000Z","size":308,"stargazers_count":37,"open_issues_count":1,"forks_count":2,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-22T00:35:27.179Z","etag":null,"topics":["cache-tags","caching","invalidation","nextjs"],"latest_commit_sha":null,"homepage":"https://next-cache-tags-redis-example.vercel.app/alphabet","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lucasconstantino.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-11-21T00:02:30.000Z","updated_at":"2025-02-12T15:21:47.000Z","dependencies_parsed_at":"2023-02-08T10:01:58.093Z","dependency_job_id":null,"html_url":"https://github.com/lucasconstantino/next-cache-tags","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lucasconstantino%2Fnext-cache-tags","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lucasconstantino%2Fnext-cache-tags/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lucasconstantino%2Fnext-cache-tags/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lucasconstantino%2Fnext-cache-tags/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lucasconstantino","download_url":"https://codeload.github.com/lucasconstantino/next-cache-tags/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248741794,"owners_count":21154374,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cache-tags","caching","invalidation","nextjs"],"created_at":"2024-10-14T23:36:28.898Z","updated_at":"2025-04-13T16:20:56.283Z","avatar_url":"https://github.com/lucasconstantino.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# Next.js Cache Tags\n\nActive ISR revalidation based on surrogate keys for Next.js\n\n[![NPM version](https://badge.fury.io/js/next-cache-tags.svg)](https://badge.fury.io/js/next-cache-tags) [![Test Coverage](https://api.codeclimate.com/v1/badges/24784ad6c2db3229d036/test_coverage)](https://codeclimate.com/github/lucasconstantino/next-cache-tags/test_coverage) [![Maintainability](https://api.codeclimate.com/v1/badges/24784ad6c2db3229d036/maintainability)](https://codeclimate.com/github/lucasconstantino/next-cache-tags/maintainability)\n\n\u003c/div\u003e\n\n## Motivation \u0026 Background\n\nThis library intends to simplify the adoption of a caching and active invalidation strategy meant for applications that have constant updates to non-personalized data/content.\n\n\u003cdetails\u003e\n  \u003csummary\u003eRead more\u003c/summary\u003e\n\n---\n\nCaching is a must for any serious application. Processing outcomes every time they are requested is not only a waste of resources that can lead to insane costs once user bases grow, it also damages the user experience: poor performance, instability, unreliability, and so on. On the context of web applications, this problem is even bigger as we entirely rely on client/server communication.\n\nVercel's Next.js is heavily dependent and encouraging of caching. Don't be mistaken: caching doesn't mean you need headers, CDNs, etc: statically built web pages that are served as is, with no further server processing, are perhaps the most aggressive form of caching we have today – and Next.js is a master at it. Anything it can transform into static files, it will.\n\nBut, any sort of caching has a huge drawback: it utterly kills dynamicity.\n\n### ♻️ Cache renewal\n\nThe only way to overcome the dynamicity loss, is to renew the cache. Putting it simple, it generally means _removing_ a cache so that further requests for that piece of information get dynamically created by the server from scratch – and eventually cached once again. But there are many competing terms and strategies here, so let's bring some clarity:\n\n- **Purge**: means _remove_ or _delete_. Upon a subsequent request, there is simply no cache and the system will naturally hit the server for a fresh data.\n- **Invalidate**: means _marking_ the cache as outdated. Upon a subsequent request, there are three usual response behaviors depending on the consumer system needs:\n  - Renew: the request goes through, acting like if no cache was there.\n  - Stale: the cache is returned, acting like if the cache was valid still.\n  - Stale while revalidate: the cached value is returned, but a parallel process goes through to the server, ensuring the cache is eventually renewed for posterior requests.\n- **Revalidate**: means actively _recreating_ a cache, even if no consumer requested the data. This is a common strategy on backend in general, when it populates a cache system – often using a Redis store – so that the computed information is promptly available for further operations that may need it.\n\n### ⚡ Fast vs. Fresh 🌱\n\nWe want ([and need](https://www.portent.com/blog/analytics/research-site-speed-hurting-everyones-revenue.htm)) websites to be _fast_. As immediate as possible. But, we also want (and need) websites to be _fresh_: outdated content being shown can cause confusion, bugs, and even direct conversion losses. Caching heavily, but renewing the cache immediately when information changes, is the solution; but it isn't an easy one to achieve.\n\nThe problem can be narrowed down to this:\n\n\u003e How can one ensure the most amount of **cache hits** possible, while also ensuring the delivery of the **latest available data** possible?\n\nYou have probably heard this quote before:\n\n\u003cblockquote\u003e\n  \u003cp\u003eThere are only two hard things in Computer Science: cache invalidation and naming things.\u003c/p\u003e\n  –– \u003ccite\u003ePhil Karlton\u003c/cite\u003e\n\u003c/blockquote\u003e\n\nThis quote might be controversial, but it summarizes well how much software engineers see this problem's complexity as a consensus.\n\n### ♜ Strategies\n\nThere are infinite ways to be smart about the invalidation problem. Different strategies for both caching and for invalidation. Their core concept will usually be: _some data changed on the data store, thus the cache must be renewed_. We'll cover a couple of common options supported by Next.js\n\n#### 1. Static Pages\n\nNext.js will [_always_](https://nextjs.org/docs/advanced-features/automatic-static-optimization) try to prerender pages on build time, and leave them be. On this strategy, the only way to update the pages is by triggering a new build – which is completely fine for small websites, but terrifying when you have thousands of pages based on content that can change regularly.\n\n#### 2. Expiration Time\n\nThe easist way possible is also the most widely used one: invalidating the cache on a fixed interval. This is what we know as Time to Live (TTL).\n\nIn Next.js, there are two main ways to implement TTL cache:\n\n##### A) `Cache-Control` header:\n\nEither set via [`headers`](https://nextjs.org/docs/api-reference/next.config.js/headers) config on `next.config.js`, or via `res.setHeader` on SSR pages, API Routes, and middlewares.\n\n##### B) `revalidate` on `getStaticProps`:\n\nThe [`revalidate`](https://nextjs.org/docs/api-reference/data-fetching/get-static-props#revalidate) return property from `getStaticProps` functions determine the amount in seconds after which the page will be re-generated. That's generally a great solution for data that doesn't change often, such as blog pages, etc.\n\n\u003e Keep in mind that this setting works using `stale-while-revalidate`, meaning that past the number of seconds set here, the first request will _trigger_ a rebuild, while still returning the stale output. Only subsequent requests will benefit from the revalidation.\n\n#### 3. On-demand Revalidation\n\nSince Next.js 12.1 [introduced on-demand Incremental Static Regeneration](https://nextjs.org/blog/next-12-1#on-demand-incremental-static-regeneration-beta), it's now possible to actively rebuild prerendered pages. This is done using the `res.revalidate` method inside API Route handlers. Usually, this means that your data store – a CMS, for instance – will dispatch a request to an API Route in your system (aka a \"webhook\"), sending as payload some information about the change made to the data, and your API Route will trigger a rebuild to any page that may have being affected by that change.\n\n## The problem\n\nDefiniting the exact pages that need rebuild upon specific data changes is a pretty complex thing to do. When you have an ecommerce, for instance, it might be very hard to determine that a product page should be rebuild when the product's price gets updated on your store, but what about other pages where this product might also be shown, such as listing pages, or even other product pages in a \"related product\" session?\n\n## The solution\n\nAlthough there are many ways to tackle this kind of problem, one of them has being widely adopted by CDNs and caching layers such as reverse proxies: tagging the cached resource with tags that identify the source data used to generate the cache. Basically, the idea consists of creating a map of tags to cached resources, so that if some data changes, we can resolve which tags were affected, and thus renew every single cached item that was originally generated using that specific data.\n\nThe following table showcases a map of cached resources (in our case, pages identified by their pathnames) and the tags used for each resource:\n\n- Given that there are 3 products in the system,\n- Given that \"Product One\" is related to \"Product Two\"\n- Given that all products are listed in the home-page\n\n| Resource\\Tag     | `products` | `product:1` | `product:2` | `product:3` | `home` |\n| ---------------- | ---------- | ----------- | ----------- | ----------- | ------ |\n| `/product-one`   | ✅         | ✅          | ✅          | ❌          | ❌     |\n| `/product-two`   | ✅         | ✅          | ✅          | ❌          | ❌     |\n| `/product-three` | ✅         | ❌          | ❌          | ✅          | ❌     |\n| `/`              | ✅         | ✅          | ✅          | ✅          | ✅     |\n\n- Invalidating `product:1` tag would re-render pages `/product-one`, `/product-two`, and `/`\n- Invalidating `product:2` tag would re-render pages `/product-one`, `/product-two`, and `/`\n- Invalidating `product:3` tag would re-render pages `/product-three` and `/`\n- Invalidating `products` would re-render all pages\n- Invalidating `home` tag would re-render page `/` only\n\n\u003e [Fastly](https://docs.fastly.com/en/guides/working-with-surrogate-keys) has a CDN well know for early supporting this technique for invalidation, and is a great source for understanding the concepts around it. While other CDNs do support it, some have being way behind in this matter for ages, such as AWS's CloudFront. In fact, [Varnish Cache](http://varnish-cache.org/) (not a scam! just an ugly website...) open-source project was perhaps the first to provide such feature, and Fastly being build on top of it is what brings it to that CDN.\n\n## This library\n\n`next-cache-tags` introduces a way to use the same strategy, but instead of depending on a reverse-proxy/CDN, it achieves that by using Next.js ISR to re-render pages statically upon data changes.\n\nThis library provides a [Redis](./src/lib/registry/redis.ts) based data-source, but you can create any other adaptor so long as it implements [`CacheTagsRegistry`](./src/lib/registry/type.ts) interface.\n\n\u003c/details\u003e\n\n---\n\n## Getting Started\n\n### 1. Install\n\n```shell\nyarn add next-cache-tags redis\n```\n\n\u003e In case you intend to create your own data-source, you don't need to install `redis`.\n\n### 2. Instantiate a client\n\n```ts\n// /src/lib/cache-tags.ts\n\nimport { CacheTags, RedisCacheTagsRegistry } from 'next-cache-tags'\n\nexport const cacheTags = new CacheTags({\n  registry: new RedisCacheTagsRegistry({\n    url: process.env.CACHE_TAGS_REDIS_URL\n  })\n})\n```\n\n### 3. Tag pages\n\nOn any page that implements `getStaticProps`, register the page with cache tags. Usually, those tags will be related to the page's content – such as a product page and related products:\n\n```ts\n// /src/pages/product/[id].tsx\n\nimport { cacheTags } from '../../lib/cache-tags'\n\ntype Product = {\n  id: string\n  name: string\n  relatedProducts: string[]\n}\n\nexport const getStaticProps = async (ctx) =\u003e {\n  const product: Product = await loadProduct(ctx.param.id)\n  const relatedProducts: Product[] = await loadProducts(product.relatedProducts)\n\n  const ids = [product.id, ...product.relatedProducts]\n  const tags = ids.map(id =\u003e `product:${id}`)\n  \n  cacheTags.register(ctx, tags)\n\n  return { props: { product, relatedProducts } }\n}\n```\n\n### 4. Create an invalidator\n\nUpon content updates, usually through webhooks, an API Route should be executed and should process the tags to invalidate.\n\n`next-cache-tags` provides a factory to create tag invalidation API Routes with ease:\n\n```ts\n// /src/pages/api/webhook.ts\n\nimport { cacheTags } from '../../lib/cache-tags'\n\nexport default cacheTags.invalidator({\n  resolver: (req) =\u003e [req.body.product.id],\n})\n```\n\nThe `resolve` configuration is a function that receives the original request, and should resolve to the list of cache-tags to be invalidated.\n\nAlternatively, you can execute such invalidations manually in any API Route:\n\n```ts\n// /src/pages/api/webhook.ts\n\nimport { cacheTags } from '../../lib/cache-tags'\n\nconst handler = (req, res) =\u003e {\n  const tags = [req.body.product.id]\n\n  // Dispatch revalidation processes.\n  cacheTags.invalidate(res, tags)\n\n  // ...do any other API Route logic\n}\n\nexport default handler\n```\n\n## Example\n\nCheckout the [./examples/redis](./examples/redis/) project for a complete, yet simple, use case. This project is deployed [here](https://next-cache-tags-redis-example.vercel.app/alphabet).\n\n## Future vision\n\n### 2023-01\n\nVercel implemented `revalidateTag` around [March 2023](https://github.com/vercel/next.js/pull/47720).\n\n\u003cdetails\u003e\n  \u003csummary\u003eSee previous comments\u003c/summary\u003e\n\nI expect that eventually Next.js will provide an API for tagging pages. As of data-source for the cache-tags registry, it could the same storage where it stores rendered pages (S3 bucket? Probably...). Alternatively, it could integrate with [Edge Config](https://vercel.com/docs/concepts/edge-network/edge-config) for ultimate availability and performance on writting/reading from the cache-tags registry.\n\nI can imagine that this could become as simple as adding an extra property to the returned object from `getStaticProps`. Something on these lines:\n\n```js\n// /src/pages/products.tsx\n\nexport const getStaticProps = async () =\u003e {\n  const products = await loadProducts()\n  const tags = products.map(product =\u003e product.id)\n\n  return {\n    tags,\n    props: {\n      products\n    }\n  }\n}\n\n// /src/pages/api/revalidate.ts\n\nexport default async function handler(req, res) {\n  await res.revalidate({ tags: [req.query.tag] })\n  return res.status(200)\n}\n\n```\n\n\u003c/details\u003e\n\n### 2023-08\n\nVercel's implementation of cache tags is **very limited** as of now. In summary, it allows one to tag resource *requests* with tags in order to revalidate them later on:\n\n```js\n// server-components\nfetch('https://...', { next: { tags: ['example'] } })\n\n// /app/api/revalidate/route.ts\nimport { revalidateTag } from 'next/cache'\n \nexport async function GET(request) {\n  revalidateTag('example')\n  return NextResponse.json({ revalidated: true, now: Date.now() })\n}\n```\n\nThis approach is falty, in that it is currently not possible to tag cache items based on their *content*, but rather on their *requesting*: by the time a homepage, for instance, requests the \"latest news\", the system has no clue which news will be returned on the response, and therefore I cannot objectively revalidate the homepage based on changes made to the news that are displayed. If a change some news title in the system, I cannot be sure this news title is shown in the homepage, therefore any news title change will need to revalidate the homepage – even changes to news items that are *not* shown in the homepage.\n\nFastly, or any reverse-proxy CDN for that matter, will always consume caching tags [based on the *response*](https://docs.fastly.com/en/guides/working-with-surrogate-keys#about-the-surrogate-key-header).\n\nTo cricumvent this problem, I can imagine something similar to the following:\n\n```js\nfetch('https://...', { next: { tags: (res) =\u003e [...] } })\n```\n\nThis would give the possibility to create precise cache tags that are based on any information responsed – be it headers, or the content payload itself.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flucasconstantino%2Fnext-cache-tags","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flucasconstantino%2Fnext-cache-tags","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flucasconstantino%2Fnext-cache-tags/lists"}