Sep 16, 2025613 words6 min read
On-Demand Revalidation using Next.js and Directus
A guide on how to use one of the most powerful tools in Next.js currently, on-demand revalidation, and connecting it with Directus.
Introduction
On-demand revalidation is a powerful feature that allows us to dynamically update static content in Next.js applications. This technique bridges the gap between static site generation (SSG) and server-side rendering (SSR), offering the best of both worlds.
Together, these tools enable a flexible, performant, and easy-to-manage content update system. This article will guide you through setting up on-demand revalidation, ensuring your Next.js app always serves the most up-to-date content from your Directus CMS.
For this guide, I'll be assuming you already have your Next.js setup with App router and your Directus instance running.
Setting up the environment
Let's start by creating an .env file at the root of your Next.js app and add REVALIDATION_SECRET key with a secret value.
REVALIDATION_SECRET="[YOUR_SECRET]"Setting up the re-validation API route in Next.js
Now let's create the API route that will handle our re-validation requests.
Create a new file at: app/api/revalidate_tag/route.ts, then place this script inside:
import { revalidateTag } from 'next/cache';
import type { NextRequest } from 'next/server';
enum Error {
InvalidSecret = 'invalid-secret',
InvalidTag = 'invalid-tag',
}
const errorMessages = {
[Error.InvalidSecret]: 'Invalid Secret',
[Error.InvalidTag]: 'Invalid Tag',
};
export async function POST(request: NextRequest) {
// data will contain the body sent in this request (we're going to send this via Directus Flow).
const data = await request.json();
// check that the secret provided matches the one in our app
const isSecretValid = data.secret === process.env.REVALIDATION_SECRET;
// check that we're receiving a tag
const isTagValid = Boolean(data.tag);
function getErrorMessage() {
if (!isSecretValid) return errorMessages[Error.InvalidSecret];
if (!isTagValid) return errorMessages[Error.InvalidTag];
}
const errorMessage = getErrorMessage();
// If there's an error, we don't re-validate
if (errorMessage) {
return Response.json({
revalidated: false,
now: Date.now(),
message: errorMessage,
});
}
// This is where the magic happens, when doing this, we're telling Next
// to purge any cache related to this tag
revalidateTag(data.tag);
return Response.json({ revalidated: true, now: Date.now() });
}Setting up request tags in Next.js
Next, we'll set up the tags for our requests, this is how Next will know what requests to purge when using validateTags.
With fetch
If you're just doing vanilla fetch request you can simply do this by setting it up on the fetch's second parameter (RequestInit):
fetch('my-request', { next: { tags: ['my-tag'] } });With custom fetch wrappers
In my case, I'm using Directus SDK (@directus/sdk) for requests, which means I need to wrap the request with Next's unstable_cache. I will be doing this in my blog article page at app/blog/[slug]/page.tsx.
I have a simple request to get a post from my posts collection via slug like this:
import { readItem } from '@directus/sdk';
import directus from '../client/directus';
export type PostData = {
slug: string;
title: string;
shortDescription?: string;
content: string;
};
export async function getPost(
slug: string,
version?: string
): Promise<PostData> {
return directus.request<PostData>(
readItem('posts', slug, {
version,
fields: [
'slug',
'title',
'shortDescription',
'content',
],
})
);
}And in my page I use it like this:
type PageProps = {
params: {
slug: string;
};
};
export default async function PostPage({ params }: PageProps) {
const post = await getPost(params.slug);
...
}So now I need to wrap the request in unstable_cache:
// Import unstable_cache
import { unstable_cache } from 'next/cache';
type PageProps = {
params: {
slug: string;
};
};
export default async function PostPage({ params }: PageProps) {
// Create unstable_cache proxy request with tag "posts" for this slug.
const getCachedPost = unstable_cache(getPost, [params.slug], {
tags: ['posts'],
});
const post = await getCachedPost(params.slug);
...
}This is pretty much it on the Next.js side of things, let's move on with the Directus setup!
Setting up Directus Flow
In your Directus frontend, head to Settings -> Flows -> Create Flow (top right).

In Flow Setup, choose a descriptive name for your flow. I'll be naming mine "Revalidate Posts"

Now in Trigger Setup:
- Choose Event Hook.
- Set Type to Action (non-blocking).
- In Scope, set when you want to trigger the flow. In my case I will be doing it on
items.create,items.updateanditems.delete - In Collections, select the collection/s that should be targeted. In my case this will be
Posts.

Now press save and we should be inside the Flow dashboard. We're now at the final step of this guide (finally).
Let's create a new operation by clicking the + symbol to the right of our newly created trigger. In the operation modal, choose a descriptive name for your operation. I will be using "Revalidation request".
- Select
Webhook / Request URL. - For Method, choose
POST - For URL, you'll want this to point to your Next.js API url, for example "/api/revalidate_tag"
- In Request Body add (remember you can change tag to whatever tag you have setup).
{
"secret": "[YOUR_REVALIDATION_SECRET]"
"tag": "posts"
}
That's it! Let's now test things out to see if this is working as expected.
Testing the Setup
Since the dev server has no cache, we need to create a build and run the build server:
npm run build && npm run startNow go to your Directus frontend and create/delete/update a new post (or whatever item for the collection you're targeting), refresh your Next.js page and your changes should be visible almost instantly!
