Deploying Webhook Revalidate for NextJS

Introduction

In previous articles, we explored various render types in NextJS and how to use the App Router. Now, I will provide a more detailed guide focusing on Incremental Static Regeneration (ISR) and Static Site Generation (SSG), specifically looking at two types of revalidation:
  • Time-based Revalidation: This method uses revalidate: {time}, automatically calling the api server to fetch new data after the specified time expires.
  • On-demand Revalidation: This approach allows you to self-define tags. To revalidate using this method, a webhook api is required. When called, it will automatically trigger the revalidation for the specified tag.

Prerequisites

This article continues the content from previous articles. If anything is unclear, please review the previous articles first.

Detail


Update the file app/products/page.tsx. The content below doesn't contain anything extraordinary. You just need to pay attention to the tags section, which demonstrates the use of On-demand revalidation.

import {PlusOutlined} from '@ant-design/icons'
import {Button} from 'antd'
import Link from 'next/link'
import type {Product} from '../_type/common'
import {callApi} from '../_util/common'
import ProductItem from './ProductItem'

async function getProducts(count: number): Promise<Product[]> {
return callApi(`products?count=${count}`, {
next: {
tags: [`products`],
},
})
}

export default async function ProductsPage() {
const products = await getProducts(10)
return (
<main className="p-8 max-w-2xl mx-auto">
<div className="flex justify-between items-center mb-6">
<h2>Product List</h2>
<Link href="/products/create">
<Button type="primary" icon={<PlusOutlined />} size="large">
Create Product
</Button>
</Link>
</div>

{products.length === 0 ? (
<div className="text-center py-10 bg-gray-50 rounded-lg border-dashed border-2">
<p className="text-gray-500">
No products found. Start by creating one!
</p>
</div>
) : (
<ul className="space-y-3">
{products.map(product => (
<ProductItem
key={product.id}
id={product.id}
productName={product.productName}
/>
))}
</ul>
)}
</main>
)
}


Update the file app/products/[id]/page.tsx. Here, I use a combination of both Time-based Revalidation and On-demand Revalidation. As a result, if we do not manually revalidate, it will also be done automatically every 60s.
import {callApi} from '@/app/_util/common'
import ProductDetailClient from './ProductDetailClient'

async function getProduct(id: string) {
return callApi(`products/${id}`, {
next: {
revalidate: 60,
tags: [`product-${id}`],
},
})
}

export default async function ProductDetailPage({params}: Props) {
const {id} = await params
const product = await getProduct(id)
return (
<main className="p-8 bg-gray-100 min-h-screen">
<ProductDetailClient initialProduct={product} />
</main>
)
}

type Props = {
params: Promise<{id: string}>
}



Create the file app/api/revalidate/route.ts with the following content.
import {revalidatePath, revalidateTag} from 'next/cache'
import {NextRequest, NextResponse} from 'next/server'

export async function POST(request: NextRequest) {
try {
const {searchParams} = request.nextUrl
const secret = searchParams.get('secret')
const tag = searchParams.get('tag')
const path = searchParams.get('path')
const type = (searchParams.get('type') as 'page' | 'layout') || 'page'

const expireValue = searchParams.get('expire')
const expire = expireValue ? +expireValue : undefined

if (secret !== process.env.SECRET_TOKEN) {
return NextResponse.json({message: 'Invalid token'}, {status: 401})
}
if (!tag && !path) {
return NextResponse.json(
{message: 'Missing tag or path parameter'},
{status: 400}
)
}
if (tag) {
if (typeof expire === 'number' && expire >= 0) {
revalidateTag(tag, {expire})
} else {
revalidateTag(tag, 'max')
}
}
if (path) {
revalidatePath(path, type)
}

return NextResponse.json({
revalidated: true,
now: Date.now(),
revalidate: tag ? 'tag' : 'path',
target: tag || path,
expire,
type,
})
} catch (err) {
return NextResponse.json(
{message: 'Error revalidating', error: String(err)},
{status: 500}
)
}
}

  • The code snippet above initializes a Route Handler to handle the Webhook revalidation. It checks the SECRET_TOKEN for security purposes, then uses query parameters (tag or path) to perform the corresponding cache clearance via revalidateTag or revalidatePath, allowing for the immediate update of new content without waiting for the cache duration to expire.
  • When using the function revalidateTag(path, profile), the profile can have values including:
    • 'max' (Recommended): Uses the stale-while-revalidate mechanism. Stale data is still returned to the current user, while Next.js silently fetches new data in the background. The new data will appear on the next access.
    • { expire: 0 }: Use this when you want the cache to expire immediately. This is commonly used for Third-party Webhooks that require data to be fresh immediately after the call.
  • When using the function revalidatePath(path, type), the type can have values including:
    • page: revalidate only for that specific page
    • layout: revalidate for all pages using the same layout. For example, the products page /products and product detail /product/{id}, if they use the same layout, will both be affected.


The result is that you can call the webhook api in a form like this:
POST /api/revalidate?secret=...&tag=product-list
POST /api/revalidate?secret=...&tag=product&expire=0
POST /api/revalidate?secret=...&path=/products/123
POST /api/revalidate?secret=...&path=/products/123&type=layout


Update the .env file.
SECRET_TOKEN = my_secret_token

  • You can set any value you want for the secret. When deploying to a production environment, please pay attention to securing this information.
  • Otherwise, this could be a vulnerability that could be exploited, where a hacker could clear all the cache from your server. For example, during periods when your page has a high volume of traffic (like flashsale events), your server could become overloaded or even crash the entire server, as there is no cache support and the server must perform a massive amount of computation to render the UI for end users.


Update the next.config.ts file to show logs when fetching data.
import type {NextConfig} from 'next'

const nextConfig: NextConfig = {
output: 'standalone',
logging: {
fetches: {
fullUrl: true,
},
},
}

export default nextConfig


When accessing a page that already has a cache, you can check the log as follows, and it will contain "cache hit".
GET /products 200 in 462ms (next.js: 90ms, application-code: 372ms)
GET http://localhost:3000/api/products?count=10 200 in 259ms (cache hit)


After calling the webhook api to revalidate:



Please check the log, and it will be "cache skip".
GET /products 200 in 622ms (next.js: 8ms, application-code: 614ms)
GET http://localhost:3000/api/products?count=10 200 in 550ms (cache skip)
Cache skipped reason: (cache-control: no-cache (hard refresh))

  • Note that when you use the api /api/revalidate?secret={{secret}}&tag=products, after it has been revalidated, when you access it, you may still see "cache hit". The reason is that the cache is only marked to be revalidated. When a user accesses it, NextJS still returns the stale data, and then it runs in the background to load the new data.
  • When you use the api /api/revalidate?secret={{secret}}&tag=products&expire=0, the cache data is deleted immediately at that time. So immediately upon accessing it again, you will be able to see the log "cache skip".


Happy coding!

Comments

Popular posts from this blog

All Practice Series

Kubernetes Deployment for Zero Downtime

Deploying a NodeJS Server on Google Kubernetes Engine

Setting up Kubernetes Dashboard with Kind

Practicing with Google Cloud Platform - Google Kubernetes Engine to deploy nginx

Using Kafka with Docker and NodeJS

Monitoring with cAdvisor, Prometheus and Grafana on Docker

Kubernetes Practice Series

Sitemap

NodeJS Practice Series