Skip to content

Secure role‑based GraphQL API with Strapi 5 & Next.js 15: Apollo, previews, ISR and revalidation

Step‑by‑step guide to install the Strapi 5 GraphQL plugin, configure role‑based access, fetch data with Apollo Client in Next.js 15, and enable editor previews, ISR and on‑demand revalidation.

By the Revisionary team · · 4 min read

turned-on grey laptop computer
Photo by Arnold Francisca on Unsplash

Use Strapi 5’s GraphQL plugin with role‑based permissions, fetch the data in Next.js 15 via Apollo Client, and enable editor previews, Incremental Static Regeneration (ISR) and on‑demand revalidation.

1. Install and enable the GraphQL plugin in Strapi 5

bash
# Using npm
npm install @strapi/plugin-graphql
# or Yarn
yarn add @strapi/plugin-graphql

After the installation, restart Strapi. The plugin appears under Plugins → GraphQL in the admin UI. No further steps are required to activate it.

a long hallway with glass doors leading to another room
Photo by Paul Hanaoka on Unsplash

2. Secure the GraphQL endpoint

2.1 Basic plugin configuration

Create or edit config/plugins.js (or config/plugins.ts if you use TypeScript) and add the GraphQL options. The most important security knobs are depthLimit, maxLimit, and disabling the Playground in production.

js
module.exports = ({ env }) => ({
  graphql: {
    config: {
      endpoint: "/graphql",
      shadowCRUD: true,
      depthLimit: 7,            // prevent overly deep queries
      defaultLimit: 25,
      maxLimit: 100,
      apolloServer: {
        tracing: false,
        // Disable GraphQL Playground / Sandbox in prod
        introspection: env("NODE_ENV") !== "production",
        landingPage: false,
      },
    },
  },
});

For a full list of options see the Strapi documentation on GraphQL configuration【1†L87-L99】.

2.2 Role‑based permissions

Strapi’s Users & Permissions plugin controls which roles can execute which GraphQL operations. Open Settings → Roles in the admin panel, select a role, and toggle the GraphQL permissions you want to expose, e.g.

  • plugin::users-permissions.user.find – read users
  • api::article.article.find – list public articles
  • api::article.article.create – allow editors to create articles

Permissions are stored as strings like plugin::users-permissions.user.create. When a GraphQL resolver has a scope defined, Strapi checks the caller’s JWT against the allowed scopes.

js
// Example resolversConfig in config/plugins.js
resolversConfig: {
  "Query.articles": { auth: { scope: ["api::article.article.find"] } },
  "Mutation.createArticle": { auth: { scope: ["api::article.article.create"] } },
},

For a deeper dive see the GraphQL advanced policies docs【1†L480-L490】.

3. Authenticate requests from Next.js

Strapi uses JWT tokens issued by the Users & Permissions plugin. To obtain a token, call the login mutation:

graphql
mutation Login($input: UsersPermissionsLoginInput!) {
  login(input: $input) {
    jwt
    user { id username }
  }
}

In Next.js you can store the JWT in an HttpOnly cookie or in local storage (only for client‑side fetches). Every GraphQL request must include the header:

code
Authorization: Bearer <jwt>

Strapi automatically validates the token and applies the role permissions defined earlier【1†L218-L229】.

4. Set up Apollo Client in a Next.js 15 project

4.1 Install dependencies

bash
npm install @apollo/client graphql
# optional TypeScript helpers
npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations

4.2 Create an Apollo client instance

Create lib/apollo.ts:

ts
import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";
import fetch from "cross-fetch";

export const createApolloClient = (token?: string) => {
  return new ApolloClient({
    ssrMode: typeof window === "undefined",
    link: new HttpLink({
      uri: process.env.NEXT_PUBLIC_STRAPI_GRAPHQL_ENDPOINT, // e.g. http://localhost:1337/graphql
      fetch,
      headers: token ? { Authorization: `Bearer ${token}` } : {},
    }),
    cache: new InMemoryCache({
      // Use a stable cache key for Strapi documents
      dataIdFromObject: (o: any) => o.documentId ? `${o.__typename}:${o.documentId}` : null,
    }),
  });
};

The dataIdFromObject function mirrors Strapi’s documentId field, ensuring cache normalization【1†L687-L692】.

4.3 Type‑safe queries with code‑generation (optional)

Add a codegen.yml:

yaml
schema: "${process.env.NEXT_PUBLIC_STRAPI_GRAPHQL_ENDPOINT}"
documents: "src/**/*.tsx"
generates:
  src/generated/graphql.tsx:
    plugins:
      - "typescript"
      - "typescript-operations"
      - "typescript-react-apollo"

Run npx graphql-codegen to generate typed hooks like useArticlesQuery.

5. Implement editor preview with Strapi’s preview feature

Strapi 5 ships a built‑in preview system that can generate a signed URL for a draft entry. Add the following to config/admin.ts:

ts
export default ({ env }) => ({
  preview: {
    enabled: true,
    config: {
      allowedOrigins: [env("CLIENT_URL")],
      async handler(uid, { documentId, locale, status }) {
        const doc = await strapi.documents(uid).findOne({ documentId });
        const { slug } = doc;
        const params = new URLSearchParams({
          secret: env("PREVIEW_SECRET"),
          slug,
          uid,
          status,
        });
        return `${env("CLIENT_URL")}/api/preview?${params}`;
      },
    },
  },
});

Set the environment variables in .env:

code
CLIENT_URL=http://localhost:3000
PREVIEW_SECRET=super‑secret-key

5.1 Next.js preview route

Create app/api/preview/route.ts (Next.js 15 app router):

ts
import { NextResponse } from "next/server";

export async function GET(request: Request) {
  const url = new URL(request.url);
  const secret = url.searchParams.get("secret");
  const slug = url.searchParams.get("slug");

  if (secret !== process.env.PREVIEW_SECRET) {
    return new NextResponse("Invalid secret", { status: 401 });
  }

  // Enable draft mode for this request
  const draftResponse = NextResponse.next();
  draftResponse.headers.set("Cache-Control", "no-store");
  draftResponse.cookies.set("nextjs-draft-mode", "1", { httpOnly: true, path: "/" });

  // Redirect to the article page
  return NextResponse.redirect(`${process.env.CLIENT_URL}/${slug}`);
}

The route validates the secret, activates draft mode, and redirects the editor to the front‑end URL.

5.2 Consume the preview flag in pages

In a page component (e.g., app/articles/[slug]/page.tsx):

tsx
import { gql } from "@apollo/client";
import { createApolloClient } from "@/lib/apollo";

export async function generateStaticParams() {
  // Fetch all slugs for published articles only
  const client = createApolloClient();
  const { data } = await client.query({
    query: gql`query AllSlugs { articles(filters: { status: { eq: "published" } }) { data { attributes { slug } } } }`,
  });
  return data.articles.data.map((a: any) => ({ slug: a.attributes.slug }));
}

export default async function ArticlePage({ params }: { params: { slug: string } }) {
  const { draftMode } = await import("next/headers");
  const isDraft = draftMode().isEnabled;

  const client = createApolloClient();
  const { data } = await client.query({
    query: gql`query Article($slug: String!, $status: String!) {
      articles(filters: { slug: { eq: $slug }, status: { eq: $status } }) {
        data { attributes { title content } }
      }
    }`,
    variables: { slug: params.slug, status: isDraft ? "draft" : "published" },
  });

  const article = data.articles.data[0].attributes;
  return (
    <article>
      <h1>{article.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: article.content }} />
    </article>
  );
}

When an editor follows the preview URL, draftMode() returns true, causing the GraphQL query to request the draft version of the entry.

6. Incremental Static Regeneration (ISR) for public pages

Next.js 15 still supports the classic pages directory ISR. In pages/articles/[slug].tsx:

tsx
import { GetStaticPaths, GetStaticProps } from "next";
import { gql } from "@apollo/client";
import { createApolloClient } from "../lib/apollo";

export const getStaticPaths: GetStaticPaths = async () => {
  const client = createApolloClient();
  const { data } = await client.query({
    query: gql`query Slugs { articles(filters: { status: { eq: "published" } }) { data { attributes { slug } } } }`,
  });
  const paths = data.articles.data.map((a: any) => ({ params: { slug: a.attributes.slug } }));
  return { paths, fallback: "blocking" };
};

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const client = createApolloClient();
  const { data } = await client.query({
    query: gql`query Article($slug: String!) { articles(filters: { slug: { eq: $slug }, status: { eq: "published" } }) { data { attributes { title content } } } }`,
    variables: { slug: params?.slug },
  });
  return {
    props: { article: data.articles.data[0].attributes },
    revalidate: 60, // ISR: refresh every minute
  };
};

export default function Article({ article }: { article: { title: string; content: string } }) {
  return (
    <article>
      <h1>{article.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: article.content }} />
    </article>
  );
}

The revalidate field tells Vercel (or any compatible platform) to rebuild the page after the specified number of seconds【7†L44-L53】.

7. On‑demand revalidation after a publish event

When an editor publishes a draft, you want to purge the ISR cache instantly. Strapi can trigger a webhook after a publish lifecycle event. Configure the webhook to call a Next.js API route that runs res.revalidate.

7.1 Strapi webhook configuration

In the Strapi admin UI, go to Settings → Webhooks → Create webhook:

  • URL: https://your‑site.com/api/revalidate
  • Events: Entry publish for the article content type

7.2 Next.js revalidation endpoint

Create pages/api/revalidate.ts:

ts
import type { NextApiRequest, NextApiResponse } from "next";

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== "POST") return res.status(405).end();
  const token = req.headers.authorization?.split(" ")[1];
  if (token !== process.env.REVALIDATE_SECRET) {
    return res.status(401).json({ message: "Invalid secret" });
  }
  const { slug } = req.body;
  try {
    await res.revalidate(`/articles/${slug}`);
    return res.json({ revalidated: true, path: `/articles/${slug}` });
  } catch (err) {
    return res.status(500).json({ message: "Revalidation error", error: err });
  }
}

Set REVALIDATE_SECRET in Vercel (or your hosting env). The endpoint receives the slug from Strapi’s webhook payload and forces a fresh generation of the page【7†L152-L170】.

8. Optional: Use Revisionary for versioned previews

If your project already uses the Revisionary plugin, you get a full audit log, trash, and per‑field history for every draft. The plugin also exposes a content API that returns the version history, which you can surface in a custom preview UI. See the Revisionary documentation pages such as /docs/reference/content-api for the exact payload structure.

9. Testing and CI/CD considerations

  1. Unit test resolvers – use jest with supertest against the GraphQL endpoint, mocking JWT headers.
  2. E2E preview flow – spin up a temporary Strapi container, create a draft, call the preview webhook, and assert that draftMode() is enabled.
  3. CI pipeline – run npm run build && npm run start in a Docker step, then execute Cypress tests that hit both the public ISR pages and the preview route.
  4. Static analysis – enable ESLint rules for GraphQL depth/complexity and for Apollo cache hygiene.

10. Observability and security hardening

  • Depth and complexity limits (depthLimit, maxLimit) guard against query‑based DoS attacks【1†L88-L96】.
  • Disable introspection in production (introspection: false) to hide the schema from unauthenticated users【1†L123-L128】.
  • Audit log – Revisionary records every publish, restore, and permission change, giving you traceability for security reviews【...†L0-L0】.
  • Rate limiting – place a reverse‑proxy (e.g., Nginx) in front of Strapi and limit requests per IP.
  • HTTPS – enforce TLS at the load balancer and set secure: true on cookies used for draft mode.
A network of interconnected cubes and thin blue lines against a light blue background
Photo by GuerrillaBuzz on Unsplash

11. Summary

By installing the GraphQL plugin, configuring role‑based permissions, and wiring Apollo Client in Next.js 15, you get a type‑safe, performant data layer. Strapi’s preview feature together with Next.js draft mode lets editors see unpublished changes instantly. ISR keeps public pages fast, while on‑demand revalidation guarantees that a newly published article is served without delay. Adding Revisionary gives you full content versioning, an audit trail, and safe restore capabilities—all essential for a production‑grade headless CMS.


The steps above assume a Strapi 5 project with the Revisionary plugin installed for versioned content. Adjust the configuration files if you are using TypeScript or a custom environment setup.

TagsStrapiGraphQLNext.jsApolloTypeScriptISR

Sources

  1. Strapi GraphQL plugin documentation
  2. Strapi Users & Permissions GraphQL API
  3. Strapi preview feature guide
  4. Next.js preview mode documentation
  5. Next.js incremental static regeneration docs
  6. Apollo Client integration with Next.js

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