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
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
# Using npm
npm install @strapi/plugin-graphql
# or Yarn
yarn add @strapi/plugin-graphqlAfter the installation, restart Strapi. The plugin appears under Plugins → GraphQL in the admin UI. No further steps are required to activate it.
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.
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 usersapi::article.article.find– list public articlesapi::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.
// 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:
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:
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
npm install @apollo/client graphql
# optional TypeScript helpers
npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations4.2 Create an Apollo client instance
Create lib/apollo.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:
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:
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:
CLIENT_URL=http://localhost:3000
PREVIEW_SECRET=super‑secret-key5.1 Next.js preview route
Create app/api/preview/route.ts (Next.js 15 app router):
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):
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:
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 publishfor thearticlecontent type
7.2 Next.js revalidation endpoint
Create pages/api/revalidate.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
- Unit test resolvers – use
jestwithsupertestagainst the GraphQL endpoint, mocking JWT headers. - E2E preview flow – spin up a temporary Strapi container, create a draft, call the preview webhook, and assert that
draftMode()is enabled. - CI pipeline – run
npm run build && npm run startin a Docker step, then execute Cypress tests that hit both the public ISR pages and the preview route. - 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: trueon cookies used for draft mode.
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.
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.