Strapi Integration with Frontend (React/Vue/Next.js/Nuxt.js)
Strapi works as a separate headless API server. The frontend communicates with it via HTTP — through REST or GraphQL. Key integration tasks: type-safe client, framework-level caching, on-demand revalidation, and proxying requests through BFF to hide API tokens.
Basic API Client
// lib/strapi.ts
const STRAPI_URL = process.env.STRAPI_URL!
const API_TOKEN = process.env.STRAPI_API_TOKEN!
interface StrapiResponse<T> {
data: T
meta: {
pagination?: { page: number; pageSize: number; pageCount: number; total: number }
}
}
export async function strapi<T>(
endpoint: string,
init?: RequestInit & { next?: { tags?: string[]; revalidate?: number } }
): Promise<StrapiResponse<T>> {
const url = `${STRAPI_URL}/api${endpoint}`
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json',
},
...init,
})
if (!response.ok) {
throw new Error(`Strapi error ${response.status}: ${await response.text()}`)
}
return response.json()
}
// Helper functions
export function getMediaURL(path: string | null | undefined) {
if (!path) return null
if (path.startsWith('http')) return path
return `${STRAPI_URL}${path}`
}
Server Components (Next.js App Router)
// app/articles/page.tsx
import { strapi } from '@/lib/strapi'
import type { Article, StrapiMedia } from '@/types/strapi'
interface ArticleListData {
id: number
attributes: {
title: string
slug: string
excerpt: string
publishedAt: string
cover: { data: { id: number; attributes: StrapiMedia } | null }
}
}
export default async function ArticlesPage({
searchParams,
}: {
searchParams: { page?: string; category?: string }
}) {
const page = Number(searchParams.page) || 1
const categoryFilter = searchParams.category
? `&filters[category][slug][$eq]=${searchParams.category}`
: ''
const { data: articles, meta } = await strapi<ArticleListData[]>(
`/articles?populate=cover,category&sort=publishedAt:desc&pagination[page]=${page}&pagination[pageSize]=12${categoryFilter}`,
{ next: { tags: ['articles'], revalidate: 3600 } }
)
return (
<div>
<ArticleGrid articles={articles} />
<Pagination meta={meta.pagination} />
</div>
)
}
export const revalidate = 3600
ISR + On-demand Revalidation
// app/articles/[slug]/page.tsx
export default async function ArticlePage({ params }: { params: { slug: string } }) {
const { data } = await strapi<ArticleListData[]>(
`/articles?filters[slug][$eq]=${params.slug}&populate=cover,author,category,tags`,
{ next: { tags: [`article-${params.slug}`] } }
)
const article = data[0]
if (!article) notFound()
return <Article article={article} />
}
export async function generateStaticParams() {
const { data } = await strapi<{ attributes: { slug: string } }[]>(
'/articles?fields[0]=slug&pagination[pageSize]=200'
)
return data.map(item => ({ slug: item.attributes.slug }))
}
// app/api/revalidate/route.ts — receives webhook from Strapi
import { revalidateTag, revalidatePath } from 'next/cache'
export async function POST(req: Request) {
const { model, entry } = await req.json()
// Invalidate by tag
revalidateTag(model) // e.g., 'article'
// Invalidate specific page
if (entry?.slug) {
revalidatePath(`/articles/${entry.slug}`)
revalidateTag(`article-${entry.slug}`)
}
return Response.json({ revalidated: true })
}
Client-side Requests (React)
// hooks/useArticles.ts
'use client'
import useSWR from 'swr'
const fetcher = (url: string) =>
fetch(url).then(r => r.json())
export function useArticles(category?: string) {
const query = category ? `?filters[category][slug][$eq]=${category}` : ''
const { data, error, isLoading } = useSWR(
`/api/articles${query}`, // via Next.js API proxy
fetcher
)
return {
articles: data?.data || [],
isLoading,
error,
}
}
// app/api/articles/route.ts — proxy for client requests
export async function GET(req: Request) {
const url = new URL(req.url)
const params = url.searchParams.toString()
const response = await fetch(
`${process.env.STRAPI_URL}/api/articles?${params}&populate=cover`,
{ headers: { Authorization: `Bearer ${process.env.STRAPI_API_TOKEN}` } }
)
const data = await response.json()
return Response.json(data)
}
TypeScript Types for Strapi Responses
// types/strapi.ts
export interface StrapiMedia {
name: string
url: string
alternativeText: string | null
width: number
height: number
formats: {
thumbnail?: StrapiMediaFormat
small?: StrapiMediaFormat
medium?: StrapiMediaFormat
large?: StrapiMediaFormat
}
}
export interface StrapiMediaFormat {
url: string
width: number
height: number
}
export interface StrapiEntity<T> {
id: number
attributes: T
}
export interface Article {
title: string
slug: string
content: string
excerpt: string
publishedAt: string
cover: { data: StrapiEntity<StrapiMedia> | null }
author: { data: StrapiEntity<{ name: string; avatar: any }> | null }
}
Timeline
Integrating Strapi with Next.js (ISR, webhooks, TypeScript types) — 2–3 days.







