We use cookies to enhance your experience on the site
CodeWorlds

Next.js and Strapi - A Powerful Frontend and Headless CMS Combination

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:

  1. Separation of concerns - frontend and backend are independent
  2. Performance - static generation with dynamic content
  3. Developer Experience - both tools are developer-friendly
  4. Scalability - easy scaling of each layer separately
  5. 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 --typescript

After 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:

  1. Go to Content-Type Builder
  2. Click Create new collection type
  3. Name your collection (e.g., "Article")
  4. 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:

  1. Go to Settings → Roless → Public
  2. Dla swojego Content Type zaznacz odpowiednie permissions (np.
    find
    i
    findOne
    )
  3. 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-frontend

Step 2: Environment variables configuration

Create the file

.env.local
:

1NEXT_PUBLIC_STRAPI_API_URL=http://localhost:1337
2STRAPI_API_TOKEN=your-api-token-here

Krok 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-graphql

Then 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-secret

2. 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/types

Summary

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, andnternationalization, and custom plugins in Strapi.

Go to CodeWorlds