Next.js course Β· Module 9: Integrations and Advanced Features
Next.js and Strapi - A Powerful Frontend and Headless CMS Combination
In this lesson9
In Quantum Metropolis, where frontend and backend technologies merge into a harmonious whole, you will discover one of the most powerful combinations in modern web development - Next.js with Strapi. This duo allows you to build scalable, performant applications with full content control.
Czym jest Strapi?
Strapi is an open-source headless CMS (Content Management System) that provides a RESTful or GraphQL API for content management. Unlike traditional CMSs like WordPress, Strapi does not impose any frontend structure - you decide how to display the data.
Kluczowe cechy Strapi:
- 100% JavaScript - napisany w Node.js
- Customizable - full control over data structure
- Self-hosted or Cloud - choose your hosting method
- RESTful & GraphQL API - flexible data access
- Admin Panel - intuitive management interface
- Roles-Based Access Control - advanced permission management
Dlaczego Next.js + Strapi?
Combining Next.js with Strapi creates a complete stack for building modern web applications:
- Separation of concerns - frontend and backend are independent
- Performance - static generation with dynamic content
- Developer Experience - both tools are developer-friendly
- Scalability - easy scaling of each layer separately
- TypeScript support - full typing support
Instalacja i konfiguracja Strapi
Krok 1: Tworzenie nowego projektu Strapi
1# Create a new Strapi project
2npx create-strapi-app@latest my-strapi-backend --quickstart
3
4# Lub z TypeScript
5npx create-strapi-app@latest my-strapi-backend --typescriptAfter installation, Strapi will automatically run on http://localhost:1337. Your first visit will redirect you to the admin account creation panel.
Krok 2: Konfiguracja Content Types
In the Strapi admin panel, you can create data structures (Content Types) using the intuitive Content-Type Builder:
- Go to Content-Type Builder
- Click Create new collection type
- Name your collection (e.g., "Article")
- Dodaj pola:
title(Text)content(Rich text)slug(UID)featuredImage(Media)author(Relation to Users)publishedAt(Datetime)
Krok 3: Konfiguracja API permissions
By default, Strapi blocks public access to the API. To enable data reading:
- Go to Settings β Roless β Public
- Dla swojego Content Type zaznacz odpowiednie permissions (np.
findifindOne) - Zapisz zmiany
Integracja Next.js ze Strapi
Krok 1: Przygotowanie projektu Next.js
1# In a new terminal, create a Next.js project
2npx create-next-app@latest my-nextjs-frontend --typescript --tailwind --app
3
4cd my-nextjs-frontendStep 2: Environment variables configuration
Create the file .env.local:
1NEXT_PUBLIC_STRAPI_API_URL=http://localhost:1337
2STRAPI_API_TOKEN=your-api-token-hereKrok 3: Tworzenie klienta API
Create helper functions for communicating with Strapi:
1// lib/strapi.ts
2interface StrapiResponse<T> {
3 data: T;
4 meta: {
5 pagination?: {
6 page: number;
7 pageSize: number;
8 pageCount: number;
9 total: number;
10 };
11 };
12}
13
14const strapiUrl = process.env.NEXT_PUBLIC_STRAPI_API_URL || 'http://localhost:1337';
15
16export async function fetchAPI<T>(
17 path: string,
18 urlParamsObject: Record<string, any> = {},
19 options: RequestInit = {}
20): Promise<T> {
21 // Merge default and user options
22 const mergedOptions: RequestInit = {
23 headers: {
24 "Content-Type": "application/json",
25 ...(process.env.STRAPI_API_TOKEN && {
26 Authorization: `Bearer ${process.env.STRAPI_API_TOKEN}`,
27 }),
28 },
29 ...options,
30 };
31
32 // Build request URL
33 const queryString = new URLSearchParams(urlParamsObject).toString();
34 const requestUrl = `${strapiUrl}${path}${queryString ? `?${queryString}` : ""}`;
35
36 // Trigger API call
37 const response = await fetch(requestUrl, mergedOptions);
38 const data = await response.json();
39
40 if (!response.ok) {
41 throw new Error(`API call failed: ${response.status}`);
42 }
43
44 return data;
45}
46
47// Helper to get media URL
48export function getStrapiMedia(url: string | null) {
49 if (url == null) return null;
50 if (url.startsWith("http") || url.startsWith("//")) return url;
51 return `${strapiUrl}${url}`;
52}Pobieranie danych ze Strapi w Next.js
Static Site Generation (SSG)
Wykorzystaj generateStaticParams i funkcje asynchroniczne w komponentach:
1// app/articles/page.tsx
2import { fetchAPI } from '@/lib/strapi';
3
4interface Article {
5 id: number;
6 attributes: {
7 title: string;
8 slug: string;
9 content: string;
10 publishedAt: string;
11 featuredImage: {
12 data: {
13 attributes: {
14 url: string;
15 alternativeText: string;
16 };
17 };
18 };
19 };
20}
21
22async function getArticles() {
23 const articlesRes = await fetchAPI<StrapiResponse<Article[]>>('/api/articles', {
24 populate: {
25 featuredImage: {
26 fields: ['url', 'alternativeText']
27 },
28 author: {
29 fields: ['name', 'email']
30 }
31 },
32 sort: ['publishedAt:desc'],
33 pagination: {
34 pageSize: 10,
35 },
36 });
37
38 return articlesRes.data;
39}
40
41export default async function ArticlesPage() {
42 const articles = await getArticles();
43
44 return (
45 <div className="container mx-auto px-4 py-8">
46 <h1 className="text-4xl font-bold mb-8">Articles</h1>
47 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
48 {articles.map((article) => (
49 <article key={article.id} className="border rounded-lg overflow-hidden shadow-lg">
50 {article.attributes.featuredImage?.data && (
51 <img
52 src={getStrapiMedia(article.attributes.featuredImage.data.attributes.url)}
53 alt={article.attributes.featuredImage.data.attributes.alternativeText}
54 className="w-full h-48 object-cover"
55 />
56 )}
57 <div className="p-4">
58 <h2 className="text-xl font-semibold mb-2">
59 {article.attributes.title}
60 </h2>
61 <p className="text-gray-600">
62 {new Date(article.attributes.publishedAt).toLocaleDateString('pl-PL')}
63 </p>
64 <a
65 href={`/articles/${article.attributes.slug}`}
66 className="text-blue-500 hover:underline mt-2 inline-block"
67 >
68 Read more β
69 </a>
70 </div>
71 </article>
72 ))}
73 </div>
74 </div>
75 );
76}Dynamic Routes
For individual articles, use dynamic routes:
1// app/articles/[slug]/page.tsx
2import { fetchAPI } from '@/lib/strapi';
3import { notFound } from 'next/navigation';
4import ReactMarkdown from 'react-markdown';
5
6interface ArticlePageProps {
7 params: Promise<{
8 slug: string;
9 }>;
10}
11
12async function getArticleBySlug(slug: string) {
13 const articlesRes = await fetchAPI<StrapiResponse<Article[]>>('/api/articles', {
14 filters: {
15 slug: {
16 $eq: slug,
17 },
18 },
19 populate: {
20 featuredImage: {
21 fields: ['url', 'alternativeText']
22 },
23 author: {
24 populate: {
25 avatar: {
26 fields: ['url']
27 }
28 }
29 }
30 },
31 });
32
33 if (!articlesRes.data || articlesRes.data.length === 0) {
34 return null;
35 }
36
37 return articlesRes.data[0];
38}
39
40export async function generateStaticParams() {
41 const articlesRes = await fetchAPI<StrapiResponse<Article[]>>('/api/articles', {
42 fields: ['slug'],
43 pagination: {
44 pageSize: 100,
45 },
46 });
47
48 return articlesRes.data.map((article) => ({
49 slug: article.attributes.slug,
50 }));
51}
52
53export default async function ArticlePage({ params }: ArticlePageProps) {
54 const article = await getArticleBySlug((await params).slug);
55
56 if (!article) {
57 notFound();
58 }
59
60 return (
61 <article className="container mx-auto px-4 py-8 max-w-4xl">
62 {article.attributes.featuredImage?.data && (
63 <img
64 src={getStrapiMedia(article.attributes.featuredImage.data.attributes.url)}
65 alt={article.attributes.featuredImage.data.attributes.alternativeText}
66 className="w-full h-96 object-cover rounded-lg mb-8"
67 />
68 )}
69
70 <h1 className="text-4xl font-bold mb-4">{article.attributes.title}</h1>
71
72 <div className="flex items-center gap-4 mb-8 text-gray-600">
73 <span>Autor: {article.attributes.author?.data?.attributes.name}</span>
74 <span>β’</span>
75 <time>
76 {new Date(article.attributes.publishedAt).toLocaleDateString('pl-PL')}
77 </time>
78 </div>
79
80 <div className="prose prose-lg max-w-none">
81 <ReactMarkdown>{article.attributes.content}</ReactMarkdown>
82 </div>
83 </article>
84 );
85}Advanced features
Real-time updates z webhooks
Strapi can send webhooks when content changes. Configure an endpoint in Next.js:
1// app/api/revalidate/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { revalidatePath, revalidateTag } from 'next/cache';
4
5export async function POST(request: NextRequest) {
6 const body = await request.json();
7 const secret = request.headers.get('x-strapi-signature');
8
9 // Weryfikuj webhook signature
10 if (secret !== process.env.STRAPI_WEBHOOK_SECRET) {
11 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
12 }
13
14 try {
15 // Revalidate appropriate paths based on the event
16 switch (body.event) {
17 case 'entry.create':
18 case 'entry.update':
19 if (body.model === 'article') {
20 revalidatePath('/articles');
21 revalidatePath(`/articles/${body.entry.slug}`);
22 }
23 break;
24 case 'entry.delete':
25 if (body.model === 'article') {
26 revalidatePath('/articles');
27 }
28 break;
29 }
30
31 return NextResponse.json({ revalidated: true });
32 } catch (error) {
33 return NextResponse.json({ error: 'Error revalidating' }, { status: 500 });
34 }
35}GraphQL integration
Strapi also offers a GraphQL API. Install the plugin:
1# W folderze Strapi
2npm install @strapi/plugin-graphqlThen use GraphQL in Next.js:
1// lib/graphql.ts
2import { GraphQLClient } from 'graphql-request';
3
4const client = new GraphQLClient(`${process.env.NEXT_PUBLIC_STRAPI_API_URL}/graphql`, {
5 headers: {
6 authorization: process.env.STRAPI_API_TOKEN
7 ? `Bearer ${process.env.STRAPI_API_TOKEN}`
8 : '',
9 },
10});
11
12export async function queryStrapi<T>(query: string, variables?: any): Promise<T> {
13 return client.request<T>(query, variables);
14}Image optimization
Integruj Strapi z Next.js Image Optimization:
1// components/StrapiImage.tsx
2import Image from 'next/image';
3import { getStrapiMedia } from '@/lib/strapi';
4
5interface StrapiImageProps {
6 src: string;
7 alt: string;
8 width: number;
9 height: number;
10 className?: string;
11}
12
13export default function StrapiImage({ src, alt, ...props }: StrapiImageProps) {
14 const imageUrl = getStrapiMedia(src);
15
16 if (!imageUrl) {
17 return null;
18 }
19
20 return (
21 <Image
22 src={imageUrl}
23 alt={alt}
24 {...props}
25 loader={({ src, width, quality }) => {
26 const params = new URLSearchParams();
27 if (width) params.set('width', width.toString());
28 if (quality) params.set('quality', quality.toString());
29 return `${src}?${params.toString()}`;
30 }}
31 />
32 );
33}Preview mode
Implement preview for unpublished content:
1// app/api/preview/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { draftMode } from 'next/headers';
4
5export async function GET(request: NextRequest) {
6 const secret = request.nextUrl.searchParams.get('secret');
7 const slug = request.nextUrl.searchParams.get('slug');
8
9 if (secret !== process.env.STRAPI_PREVIEW_SECRET || !slug) {
10 return NextResponse.json({ message: 'Invalid token' }, { status: 401 });
11 }
12
13 // Enable Draft Mode
14 (await draftMode()).enable();
15
16 // Redirect to the preview page
17 return NextResponse.redirect(new URL(`/articles/${slug}`, request.url));
18}
19
20// app/api/exit-preview/route.ts
21export async function GET() {
22 (await draftMode()).disable();
23 return NextResponse.redirect('/');
24}Deployment i najlepsze praktyki
1. Environment Variables
Always use environment variables for sensitive data:
1# .env.production
2NEXT_PUBLIC_STRAPI_API_URL=https://your-strapi-instance.com
3STRAPI_API_TOKEN=your-production-token
4STRAPI_WEBHOOK_SECRET=your-webhook-secret
5STRAPI_PREVIEW_SECRET=your-preview-secret2. Caching Strategy
Wykorzystaj Next.js caching:
1// Cache na 60 sekund
2export const revalidate = 60;
3
4// Or use cache tags
5fetch(url, { next: { tags: ['articles'] } });3. Error Handling
Implement robust error handling:
1export async function getArticles() {
2 try {
3 const data = await fetchAPI<StrapiResponse<Article[]>>('/api/articles');
4 return data;
5 } catch (error) {
6 console.error('Error fetching articles:', error);
7 return { data: [], meta: {} };
8 }
9}4. TypeScript Types
Generuj typy ze Strapi schema:
1# Install the type generator
2npm install -D @strapi/typesSummary
Combining Next.js with Strapi creates a powerful stack for building modern web applications. Strapi provides a flexible API and intuitive content management system, while Next.js delivers a performant frontend with SSG/SSR and optimizations.
Ta kombinacja jest idealna dla:
- Blogs and news portals
- E-commerce
- Portfolio i stron korporacyjnych
- Aplikacji SaaS
- Platform edukacyjnych
In the following modules, we will explore more advanced topics such as multi-tenancy, internationalization, and custom plugins in Strapi.
Code for this lesson: App.tsx
1import React, { useState } from 'react';
2
3interface Article {
4 id: number;
5 title: string;
6 slug: string;
7 content: string;
8 author: string;
9 publishedAt: string;
10 category: string;
11}
12
13const mockArticles: Article[] = [
14 { id: 1, title: 'Introduction to Headless CMS', slug: 'intro-headless-cms', content: 'Headless CMS separates the backend from the frontend, giving full control over content presentation...', author: 'Admin', publishedAt: '2025-01-15', category: 'CMS' },
15 { id: 2, title: 'Strapi and Next.js - Getting Started', slug: 'strapi-nextjs-start', content: 'Integrating Strapi with Next.js allows building performant applications with dynamic content...', author: 'Developer', publishedAt: '2025-02-10', category: 'Tutorial' },
16 { id: 3, title: 'REST vs GraphQL in Strapi', slug: 'rest-vs-graphql', content: 'Strapi offers both REST API and GraphQL. Each approach has its advantages...', author: 'Admin', publishedAt: '2025-03-05', category: 'API' },
17 { id: 4, title: 'Typing Strapi Responses in TypeScript', slug: 'strapi-typescript', content: 'With TypeScript we can type Strapi API responses for better DX...', author: 'Developer', publishedAt: '2025-03-20', category: 'TypeScript' },
18];
19
20const StrapiDemo = () => {
21 const [articles] = useState<Article[]>(mockArticles);
22 const [selected, setSelected] = useState<Article | null>(null);
23 const [endpoint, setEndpoint] = useState('/api/articles');
24 const [response, setResponse] = useState<string>('');
25
26 const simulateFetch = () => {
27 const isDetail = endpoint.includes('/api/articles/');
28 const slug = endpoint.split('/api/articles/')[1];
29
30 if (isDetail && slug) {
31 const found = articles.find(a => a.slug === slug);
32 if (found) {
33 setResponse(JSON.stringify({ data: { id: found.id, attributes: { title: found.title, content: found.content, slug: found.slug, author: found.author, publishedAt: found.publishedAt } }, meta: {} }, null, 2));
34 setSelected(found);
35 } else {
36 setResponse(JSON.stringify({ error: { status: 404, message: 'Not found' } }, null, 2));
37 }
38 } else {
39 setResponse(JSON.stringify({ data: articles.map(a => ({ id: a.id, attributes: { title: a.title, slug: a.slug, category: a.category, publishedAt: a.publishedAt } })), meta: { pagination: { page: 1, pageSize: 25, total: articles.length } } }, null, 2));
40 setSelected(null);
41 }
42 };
43
44 return (
45 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#fff', fontFamily: 'sans-serif' }}>
46 <h1 style={{ color: '#64ffda', textAlign: 'center' }}>Next.js + Strapi CMS</h1>
47 <p style={{ textAlign: 'center', color: '#888', marginBottom: '20px' }}>Headless CMS API Simulation</p>
48
49 <div style={{ maxWidth: '800px', margin: '0 auto' }}>
50 <div style={{ background: 'rgba(255,255,255,0.05)', padding: '16px', borderRadius: '10px', marginBottom: '16px', border: '1px solid #333' }}>
51 <h3 style={{ color: '#7c4dff', marginBottom: '10px' }}>API Explorer</h3>
52 <div style={{ display: 'flex', gap: '8px', marginBottom: '10px' }}>
53 <input value={endpoint} onChange={e => setEndpoint(e.target.value)}
54 style={{ flex: 1, padding: '10px', background: '#16213e', border: '1px solid #444', borderRadius: '6px', color: '#fff', fontFamily: 'monospace' }} />
55 <button onClick={simulateFetch} style={{ padding: '10px 20px', background: '#7c4dff', border: 'none', borderRadius: '6px', color: '#fff', cursor: 'pointer' }}>
56 GET
57 </button>
58 </div>
59 <div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
60 <button onClick={() => setEndpoint('/api/articles')} style={{ padding: '4px 10px', background: 'rgba(100,255,218,0.1)', border: '1px solid #64ffda', borderRadius: '4px', color: '#64ffda', cursor: 'pointer', fontSize: '12px' }}>/api/articles</button>
61 {articles.map(a => (
62 <button key={a.id} onClick={() => setEndpoint('/api/articles/' + a.slug)} style={{ padding: '4px 10px', background: 'rgba(124,77,255,0.1)', border: '1px solid #7c4dff', borderRadius: '4px', color: '#7c4dff', cursor: 'pointer', fontSize: '12px' }}>
63 /articles/{a.slug}
64 </button>
65 ))}
66 </div>
67 </div>
68
69 {response && (
70 <div style={{ background: '#000', padding: '16px', borderRadius: '10px', marginBottom: '16px' }}>
71 <h4 style={{ color: '#4caf50', marginBottom: '8px' }}>Response:</h4>
72 <pre style={{ color: '#0f0', fontSize: '12px', overflow: 'auto', maxHeight: '250px', whiteSpace: 'pre-wrap' }}>{response}</pre>
73 </div>
74 )}
75
76 {selected && (
77 <div style={{ background: 'rgba(255,255,255,0.05)', padding: '20px', borderRadius: '10px', border: '1px solid #333' }}>
78 <span style={{ color: '#7c4dff', fontSize: '12px' }}>{selected.category}</span>
79 <h2 style={{ color: '#64ffda', margin: '8px 0' }}>{selected.title}</h2>
80 <p style={{ color: '#aaa', fontSize: '13px' }}>By {selected.author} | {selected.publishedAt}</p>
81 <p style={{ color: '#ccc', marginTop: '12px', lineHeight: '1.6' }}>{selected.content}</p>
82 </div>
83 )}
84 </div>
85 </div>
86 );
87};
88
89export default StrapiDemo;Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What data does Strapi return by default through its REST API?
2. Which of the following features BEST describes a Headless CMS?
Hands-on tasks in the game
- Click in order
Arrange the steps of integrating Next.js with Strapi in order.
- Code editor
Create a component that fetches data from headless CMS Strapi in Next.js
- Code editor
Create an Article content type with title, content, and slug fields.