How to rollback content in Strapi 5 for a Next.js 15 frontend
Learn step‑by‑step how to enable Strapi 5 content history, fetch version data via GraphQL or REST, and implement reliable rollback in a Next.js 15 + React + TypeScript app, with testing and CI/CD tips.
By the Revisionary team · · 5 min read
Strapi 5 can store every change you make to versioned content types, and Next.js 15 can re‑validate static pages when a draft is published. By combining Strapi’s Content History (provided by the Revisionary plugin) with the Document Service API, GraphQL, or the REST API you can build a reliable rollback mechanism that works from the admin UI down to the front‑end code.
1. Prerequisites and what the feature does
- Strapi 5 installed on Node 18‑24 with a supported database (PostgreSQL, MySQL or SQLite).
- The Revisionary plugin active (Growth or Enterprise plan) – it records field‑level differences, provides a version timeline, and stores a trash for deleted entries.
- A Next.js 15 project written in React and TypeScript.
- Basic knowledge of Strapi’s Draft & Publish workflow.
When these conditions are met, every edit made through the Strapi admin panel creates a new version that can be listed, compared, and restored. The front‑end can query the current published version, a specific draft, or any historical snapshot, making rollback a matter of a few API calls.
2. Enable Draft & Publish and Content History
- Activate Draft & Publish for the content types you want to version (e.g.,
article). In the Strapi Content‑Type Builder toggle Draft & Publish. - Install Revisionary from the Marketplace, then run the installer as described in the /docs/getting-started/install guide. The plugin adds a Version history panel on the right side of the Content Manager.
- Configure retention if you need a shorter window than the default (Growth = 14 days, Enterprise = 30 days). Add to
/config/admin.js:
module.exports = ({ env }) => ({
history: { retentionDays: 7 }, // keep versions for a week
});The setting cannot be extended beyond the plan limits, but shortening it helps control storage.
3. Listing versions – the API endpoints
3.1 Content‑Manager endpoint (admin only)
The admin UI calls an internal endpoint that can also be used from a server‑side script:
GET /content-manager/history-versions?contentType=api::article.article&documentId=abc123It returns a paginated array of version objects containing id, createdAt, author, and a data snapshot of the fields at that moment.
3.2 Document Service API – version‑aware queries
Strapi’s Document Service lets you query a specific version by passing the status parameter:
// Fetch the published version
const published = await strapi.documents('api::article.article').findOne({
documentId: 'abc123',
status: 'published',
});
// Fetch a draft (the latest unpublished changes)
const draft = await strapi.documents('api::article.article').findOne({
documentId: 'abc123',
status: 'draft',
});The status enum is documented in the /docs/reference/content-api page.
3.3 GraphQL – status & publicationFilter
When you use the GraphQL plugin you can request a specific version directly in the query:
query GetArticle($id: ID!, $status: PublicationStatus!) {
article(id: $id, status: $status) {
title
content
updatedAt
}
}Supported PublicationStatus values are DRAFT and PUBLISHED. You can also filter by the relationship between draft and published versions using publicationFilter, for example MODIFIED to get drafts that have unpublished changes.
3.4 REST – explicit draft handling
For the classic REST routes, add ?status=draft to keep the document in draft mode; omitting the parameter publishes immediately.
PUT /api/articles/123?status=draft # keeps as draft
PUT /api/articles/123 # publishes instantlySee the official snippet at /docs/reference/content-api.
4. Implementing rollback in a Next.js 15 front‑end
4.1 Architecture overview
- API layer – a thin Next.js API route (
/pages/api/rollback.ts) that talks to Strapi either via GraphQL or the Document Service REST endpoints. - Client side – a React component in the admin dashboard that lists available versions and triggers the rollback API.
- Revalidation – after a successful rollback the route calls
res.revalidate('/articles/[slug]')so ISR pages are regenerated.
4.2 TypeScript helper for fetching versions
Create a utility module lib/strapi.ts:
import fetch from 'node-fetch';
const STRAPI_URL = process.env.STRAPI_URL!;
const TOKEN = process.env.STRAPI_TOKEN!;
export async function fetchVersions(documentId: string) {
const res = await fetch(
`${STRAPI_URL}/content-manager/history-versions?contentType=api::article.article&documentId=${documentId}`,
{
headers: { Authorization: `Bearer ${TOKEN}` },
}
);
if (!res.ok) throw new Error('Failed to load versions');
return res.json(); // { data: Version[], meta: {...} }
}
export async function restoreVersion(documentId: string, versionId: string) {
const res = await fetch(
`${STRAPI_URL}/content-manager/restore-version`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${TOKEN}`,
},
body: JSON.stringify({ documentId, versionId }),
}
);
if (!res.ok) throw new Error('Rollback failed');
return res.json();
}The restore-version endpoint is part of Revisionary’s internal API; it copies the selected snapshot into the current draft and marks the entry as Modified.
4.3 API route that triggers the rollback and revalidation
File: pages/api/articles/[id]/rollback.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { restoreVersion } from '@/lib/strapi';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') return res.status(405).end();
const { id } = req.query as { id: string };
const { versionId } = req.body as { versionId: string };
try {
await restoreVersion(id, versionId);
// Re‑validate the ISR page that displays the article
await res.revalidate(`/articles/${id}`);
return res.status(200).json({ message: 'Rollback successful and page revalidated' });
} catch (e) {
console.error(e);
return res.status(500).json({ error: 'Rollback failed' });
}
}Next.js 15’s res.revalidate works on Vercel, Netlify Edge, or any platform that supports On‑Demand Revalidation.
4.4 Front‑end UI component
import { useEffect, useState } from 'react';
import { fetchVersions, restoreVersion } from '@/lib/strapi';
interface Version { id: string; createdAt: string; data: Record<string, any>; }
export default function VersionRollback({ articleId }: { articleId: string }) {
const [versions, setVersions] = useState<Version[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
fetchVersions(articleId).then((res) => setVersions(res.data));
}, [articleId]);
const handleRollback = async (versionId: string) => {
setLoading(true);
await fetch('/api/articles/' + articleId + '/rollback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ versionId }),
});
setLoading(false);
// Optionally refresh UI after rollback
window.location.reload();
};
return (
<section>
<h2>Version history</h2>
{loading && <p>Rolling back…</p>}
<ul>
{versions.map((v) => (
<li key={v.id}>
{new Date(v.createdAt).toLocaleString()} –
<button onClick={() => handleRollback(v.id)} disabled={loading}>
Roll back to this version
</button>
</li>
))}
</ul>
</section>
);
}The component reads the version list via the helper, displays timestamps, and calls the API route to perform the restore.
5. GraphQL‑only rollback (optional)
If your team prefers a pure GraphQL flow, you can expose a custom mutation that internally calls the Revisionary service. Add to ./src/api/article/services/article.js:
module.exports = {
async rollback(ctx) {
const { documentId, versionId } = ctx.request.body;
await strapi.service('plugin::revisionary.version').restore({ documentId, versionId });
return { ok: true };
},
};Then register the mutation in ./src/api/article/config/schema.graphql:
extend type Mutation {
rollbackArticle(documentId: ID!, versionId: ID!): RollbackResponse!
}
type RollbackResponse { ok: Boolean! }Clients can now run:
mutation Rollback($doc: ID!, $ver: ID!) {
rollbackArticle(documentId: $doc, versionId: $ver) {
ok
}
}After the mutation succeeds, trigger a revalidation request from the front‑end (e.g., using fetch('/api/revalidate?slug=article-slug')).
6. Testing the rollback flow
6.1 Unit tests for the API route
import handler from '@/pages/api/articles/[id]/rollback.ts';
import { createMocks } from 'node-mocks-http';
test('POST triggers restore and revalidation', async () => {
const { req, res } = createMocks({
method: 'POST',
url: '/api/articles/123/rollback',
body: { versionId: 'v456' },
});
await handler(req, res);
expect(res._getStatusCode()).toBe(200);
const data = JSON.parse(res._getData());
expect(data.message).toContain('successful');
});The test ensures the route returns 200 and that the JSON shape matches expectations. You can mock restoreVersion and res.revalidate with Jest spies.
6.2 Integration test using the Document Service
import { Strapi } from '@strapi/strapi';
test('restore creates a new draft identical to chosen version', async () => {
const strapi = await Strapi().load();
const articleId = 'abc123';
const versions = await strapi.documents('api::article.article').findMany({
documentId: articleId,
populate: '*',
status: 'draft',
});
const target = versions[0];
// Simulate a rollback
await strapi.service('plugin::revisionary.version').restore({ documentId: articleId, versionId: target.id });
const after = await strapi.documents('api::article.article').findOne({ documentId: articleId, status: 'draft' });
expect(after.title).toBe(target.data.title);
});The integration test runs against a temporary Strapi instance (started in CI) and validates that the restored draft matches the snapshot.
7. CI/CD considerations
- Run end‑to‑end tests in GitHub Actions before deploying. Example snippet:
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: strapi
POSTGRES_PASSWORD: strapi
POSTGRES_DB: strapi
ports: ['5432:5432']
steps:
- uses: actions/checkout@v4
- name: Install deps
run: yarn install --frozen-lockfile
- name: Build Strapi
run: yarn build:api
- name: Start Strapi
run: yarn start &
env:
DATABASE_CLIENT: postgres
DATABASE_HOST: localhost
DATABASE_PORT: 5432
DATABASE_NAME: strapi
DATABASE_USERNAME: strapi
DATABASE_PASSWORD: strapi
- name: Run tests
run: yarn test- Deploy Next.js on Vercel (or any platform) and add a webhook in Strapi that calls the Vercel
/api/revalidateendpoint after a successful publish or rollback. The webhook payload can be filtered to only fire for thearticlecontent type. - Version‑aware caching – set
Cache-Control: stale-while-revalidateon the GraphQL fetches so that browsers keep showing the last published page while the new one is being regenerated.
8. Observability and audit logging
When you enable the audit‑logs plugin (Enterprise) Strapi records every version‑restore, publish, and delete action. You can stream those logs to an external system (ELK, Datadog) by adding a custom middleware:
module.exports = () => async (ctx, next) => {
await next();
const { method, url, state } = ctx;
if (method === 'POST' && url.includes('/restore-version')) {
console.log(`[audit] ${state.user?.email} restored version ${ctx.request.body.versionId}`);
}
};The logs help you answer compliance questions and quickly spot unexpected rollbacks.
9. Putting it all together
- Enable Draft & Publish and install Revisionary.
- Configure retention to match your data‑retention policy.
- Use the Document Service API (or GraphQL) to fetch the current published version and any drafts you need.
- Build a Next.js API route that calls the Revisionary
restore-versionendpoint, then triggers ISR revalidation. - Add a React component in your editorial UI to list versions and fire the rollback request.
- Write unit and integration tests that mock the Strapi client and verify the draft state after a restore.
- Hook the flow into your CI/CD pipeline and set up audit‑log monitoring for production visibility.
Following these steps gives you a production‑ready rollback mechanism that works across REST, GraphQL, and the Document Service, while keeping the front‑end in sync through Next.js ISR. The same pattern can be extended to other content types—just enable versioning for them in Revisionary’s settings.
The article references the official Strapi documentation and the Revisionary plugin’s user guide. For deeper details on each API, see the linked docs.
Sources
The Revisionary team
Developers who build Revisionary, a version history plugin for Strapi 5. We write about Strapi, web development, AI and application security.
This article was written with the help of AI.