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.
Route Handler boundary for client search
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.
Related FreeMac guides
- For populate details, read Strapi 5 Populate Guide: Relations, Media, and Nested Queries.
- For App Router routes, read Next.js Routing Guide: App Router Patterns.
- For Server and Client Components, read Next.js Server vs Client Components.
Continue reading
Strapi 5 Populate Guide: Relations, Media, and Nested Queries
Use Strapi 5 REST populate correctly for relations, media, components, nested queries, field selection, qs query builders, and safer production response sizes.
9 minReact Hydration Failed: Causes, Debugging, and Fixes
Debug server and client rendering mismatches caused by dates, random values, browser APIs, invalid HTML, local storage, locale differences, and DOM-changing extensions.
9 minNext.js Server Actions vs Fetch: How to Choose
Choose between Server Actions, client fetch, Route Handlers, and backend APIs based on authentication, forms, caching, uploads, API reuse, and request control.
Subscribe to FreeMac
Weekly picks: free Mac software reviews, trusted source updates, alternatives, and low-friction guides.