Next.jsStrapiREST APITypeScript

Next.js with Strapi 5: Fetching, Cache, and Security

Connect Next.js App Router to Strapi 5 safely with server-side fetch helpers, populate queries, cache rules, draft preview boundaries, media URLs, and private API tokens.

·Updated ·12 min read·Counting...
Next.js with Strapi 5: Fetching, Cache, and Security

The safest default for connecting Next.js to Strapi is simple: keep pages and layouts as Server Components, call Strapi from a server-side data layer, and expose only the fields the browser needs. Private Strapi API tokens must stay in server environment variables and must not use the NEXT_PUBLIC_ prefix.

Environment variables

STRAPI_URL=http://localhost:1337
STRAPI_API_TOKEN=replace-with-server-only-token

Public content may not require a token if the Strapi Public role has read-only permission. Draft previews, member content, and private fields should use server-side authentication.

A shared fetch helper

const STRAPI_URL = process.env.STRAPI_URL

if (!STRAPI_URL) {
  throw new Error("Missing STRAPI_URL")
}

type StrapiResponse<T> = {
  data: T
  meta?: Record<string, unknown>
}

export async function strapiFetch<T>(
  path: string,
  options: RequestInit & { next?: { revalidate?: number; tags?: string[] } } = {},
): Promise<StrapiResponse<T>> {
  const token = process.env.STRAPI_API_TOKEN
  const response = await fetch(`${STRAPI_URL}${path}`, {
    ...options,
    headers: {
      Accept: "application/json",
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
      ...options.headers,
    },
    next: options.next ?? { revalidate: 300 },
  })

  if (!response.ok) {
    throw new Error(`Strapi ${response.status}: ${await response.text()}`)
  }

  return response.json()
}

Centralizing fetch behavior keeps auth, caching, errors, and logging in one place.

Fetch a list in a Server Component

type Article = {
  documentId: string
  title: string
  slug: string
  excerpt: string
}

export default async function BlogPage() {
  const { data: articles } = await strapiFetch<Article[]>(
    "/api/articles?fields[0]=title&fields[1]=slug&fields[2]=excerpt",
    { next: { revalidate: 300, tags: ["articles"] } },
  )

  return (
    <ul>
      {articles.map((article) => (
        <li key={article.documentId}>{article.title}</li>
      ))}
    </ul>
  )
}

Strapi 5 commonly returns documentId and a flatter data shape than Strapi 4's attributes examples. Confirm the version before copying response types.

Detail queries and relations

Use qs for complex queries instead of hand-writing long bracket URLs:

import qs from "qs"

const query = qs.stringify(
  {
    filters: { slug: { $eq: slug } },
    fields: ["title", "slug", "excerpt", "content"],
    populate: {
      cover: { fields: ["url", "alternativeText"] },
      author: { fields: ["name"] },
    },
  },
  { encodeValuesOnly: true },
)

const { data } = await strapiFetch<Article[]>(`/api/articles?${query}`)
const article = data[0]

Define separate query shapes for list and detail pages. Do not make a list page download full article bodies just to reuse one function.

Cache choices

Content type Suggested cache behavior
Public articles that rarely change revalidate
User-specific private data cache: "no-store"
Content that must refresh on publish cache tags plus webhook revalidation
Draft preview dynamic request, separate from public cache

Do not put personalized or private token-based responses into a public cache without understanding the deployment platform's cache behavior.

If browser interaction needs Strapi data, call your own Route Handler:

import { NextRequest, NextResponse } from "next/server"

export async function GET(request: NextRequest) {
  const query = request.nextUrl.searchParams.get("q")?.trim() ?? ""

  if (query.length < 2) {
    return NextResponse.json({ data: [] })
  }

  const result = await searchArticles(query)
  return NextResponse.json(result)
}

This lets you validate input, rate-limit, restrict returned fields, and keep private tokens server-side.

Media URLs

Strapi media fields may return relative URLs:

export function strapiMedia(url?: string) {
  if (!url) return undefined
  if (url.startsWith("http")) return url
  return `${process.env.STRAPI_URL}${url}`
}

When using next/image, allow the media host in Next config. For production, consider object storage or a CDN for media assets.

Subscribe to FreeMac

Weekly picks: free Mac software reviews, trusted source updates, alternatives, and low-friction guides.