Skip to content

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

turned-on monitor
Photo by Stephen Phillips - Hostreviews.co.uk on Unsplash

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

  1. Activate Draft & Publish for the content types you want to version (e.g., article). In the Strapi Content‑Type Builder toggle Draft & Publish.
  2. 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.
  3. Configure retention if you need a shorter window than the default (Growth = 14 days, Enterprise = 30 days). Add to /config/admin.js:
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:

code
GET /content-manager/history-versions?contentType=api::article.article&documentId=abc123

It 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:

ts
// 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:

graphql
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.

bash
PUT /api/articles/123?status=draft   # keeps as draft
PUT /api/articles/123                # publishes instantly

See the official snippet at /docs/reference/content-api.

4. Implementing rollback in a Next.js 15 front‑end

4.1 Architecture overview

  1. 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.
  2. Client side – a React component in the admin dashboard that lists available versions and triggers the rollback API.
  3. 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:

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

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

tsx
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.

Computer screen displaying HTML code for a web development project
Photo by Mohammad Rahmani on Unsplash

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:

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:

graphql
extend type Mutation {
  rollbackArticle(documentId: ID!, versionId: ID!): RollbackResponse!
}

type RollbackResponse { ok: Boolean! }

Clients can now run:

graphql
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

ts
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

ts
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

  1. Run end‑to‑end tests in GitHub Actions before deploying. Example snippet:
yaml
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
  1. Deploy Next.js on Vercel (or any platform) and add a webhook in Strapi that calls the Vercel /api/revalidate endpoint after a successful publish or rollback. The webhook payload can be filtered to only fire for the article content type.
  2. Version‑aware caching – set Cache-Control: stale-while-revalidate on 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:

js
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

  1. Enable Draft & Publish and install Revisionary.
  2. Configure retention to match your data‑retention policy.
  3. Use the Document Service API (or GraphQL) to fetch the current published version and any drafts you need.
  4. Build a Next.js API route that calls the Revisionary restore-version endpoint, then triggers ISR revalidation.
  5. Add a React component in your editorial UI to list versions and fire the rollback request.
  6. Write unit and integration tests that mock the Strapi client and verify the draft state after a restore.
  7. 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.

TagsStrapi 5Next.js 15Content versioningRollbackTypeScriptGraphQL

Sources

  1. Content History feature – Strapi docs
  2. Document Service status API – Strapi docs
  3. GraphQL API reference – Strapi docs
  4. REST API status snippet – Strapi docs
  5. Draft & Publish feature – Strapi docs

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.

All articles