strapi + next.js

Next.js and Strapi Client SDK Example

How to get started with Strpai and Next.js 16 using Strapi Client SDK.

Strapi Client SDK Integration Guide

This comprehensive guide explains how we integrate the @strapi/client SDK in our Next.js application, covering architecture, authentication, error handling, and usage patterns from beginner to advanced implementations.

Table of Contents


Architecture Overview

flowchart TB
    subgraph "Next.js App"
        Page[Page Component]
        DataLayer[Data Layer]
        SDK[Strapi SDK Wrapper]
        ErrorBoundary[Global Error Boundary]
        NotFound[Not Found Page]
    end

    subgraph "Strapi Backend"
        API[Strapi API]
    end

    Page --> DataLayer
    DataLayer --> SDK
    SDK --> API
    SDK -.->|StrapiError| ErrorBoundary
    Page -.->|notFound| NotFound

    style SDK fill:#f9f,stroke:#333
    style ErrorBoundary fill:#ff9,stroke:#333
    style NotFound fill:#6bcb77,stroke:#333

File Structure

src/
├── lib/
│   ├── strapi-sdk.ts         # Advanced SDK wrapper with error handling
│   └── strapi-sdk-simple.ts  # Simple SDK for beginners
├── data/
│   ├── index.ts              # API exports
│   └── landing-page.ts       # Page-specific data fetchers
└── app/
    ├── page.tsx              # Page component
    ├── global-error.tsx      # Error boundary
    └── not-found.tsx         # 404 page

Basic Implementation

For beginners, start with a simple SDK wrapper without complex error handling. Errors will propagate naturally to the calling code.

Simple SDK (strapi-sdk-simple.ts)

import { strapi } from '@strapi/client';

// Create the Strapi client
const client = strapi({
  baseURL: "http://localhost:1337/api",
});

/**
 * Fetch a single type (e.g., homepage, settings)
 */
export async function getSingleType(name: string) {
  const { data } = await client.single(name).find();
  return data;
}

/**
 * Fetch all items from a collection (e.g., articles, products)
 */
export async function getCollection(name: string) {
  const { data } = await client.collection(name).find();
  return data;
}

/**
 * Fetch a single item from a collection by ID
 */
export async function getDocument(collection: string, id: string) {
  const { data } = await client.collection(collection).findOne(id);
  return data;
}

/**
 * Fetch a single item from a collection by slug
 */
export async function getDocumentBySlug(collection: string, slug: string) {
  const { data } = await client.collection(collection).find({
    filters: { slug: { $eq: slug } },
  });
  return data[0] ?? null;
}

Basic Usage with Try/Catch

// In your page or component
async function loadArticle(slug: string) {
  try {
    const article = await getDocumentBySlug("articles", slug);
    if (!article) {
      // Handle not found
      return null;
    }
    return article;
  } catch (error) {
    console.error("Failed to load article:", error);
    // Handle error (show message, redirect, etc.)
    return null;
  }
}

Basic Error Flow

sequenceDiagram
    participant Page
    participant SDK
    participant Strapi

    Page->>SDK: getCollection("articles")
    SDK->>Strapi: HTTP GET /api/articles

    alt Success
        Strapi-->>SDK: 200 OK + Data
        SDK-->>Page: Data
    else Error
        Strapi-->>SDK: Error Response
        SDK-->>Page: Throws Error
        Page->>Page: catch(error)
    end

Advanced Implementation

For production applications, use a custom error class and centralized error handling.

Custom Error Class

class StrapiError extends Error {
  constructor(
    message: string,
    public readonly contentType: string,
    public readonly cause?: unknown
  ) {
    super(message);
    this.name = 'StrapiError';
  }
}

This custom error provides:

  • message: Human-readable error description
  • contentType: The Strapi content type that failed (useful for logging)
  • cause: The original error for debugging

Client Factory

const createClient = (config?: Omit<Config, 'baseURL'>) => {
  const clientConfig: Config = {
    baseURL: getStrapiURL() + "/api",
    ...config,
  };

  // Only add auth if token exists and not overridden by config
  const token = process.env.STRAPI_API_TOKEN;

  if (token && !config?.auth) {
    clientConfig.auth = token;
  }

  return strapi(clientConfig);
};

Advanced SDK (strapi-sdk.ts)

import { strapi } from '@strapi/client';
import type { API, Config } from '@strapi/client';
import { draftMode } from 'next/headers';
import { getStrapiURL } from "@/lib/utils";

class StrapiError extends Error {
  constructor(
    message: string,
    public readonly contentType: string,
    public readonly cause?: unknown
  ) {
    super(message);
    this.name = 'StrapiError';
  }
}

const createClient = (config?: Omit<Config, 'baseURL'>) => {
  const clientConfig: Config = {
    baseURL: getStrapiURL() + "/api",
    ...config,
  };

  const token = process.env.STRAPI_API_TOKEN;

  if (token && !config?.auth) {
    clientConfig.auth = token;
  }

  return strapi(clientConfig);
};

/**
 * Fetches a collection type from Strapi.
 * @throws {StrapiError} When the fetch fails
 */
export async function fetchCollectionType<T = API.Document[]>(
  collectionName: string,
  options?: API.BaseQueryParams,
  config?: Omit<Config, 'baseURL'>
): Promise<T> {
  const { isEnabled: isDraftMode } = await draftMode();

  try {
    const { data } = await createClient(config)
      .collection(collectionName)
      .find({
        ...options,
        status: isDraftMode ? 'draft' : 'published',
      });

    return data as T;
  } catch (error) {
    throw new StrapiError(
      `Failed to fetch collection "${collectionName}"`,
      collectionName,
      error
    );
  }
}

/**
 * Fetches a single type from Strapi.
 * @throws {StrapiError} When the fetch fails
 */
export async function fetchSingleType<T = API.Document>(
  singleTypeName: string,
  options?: API.BaseQueryParams,
  config?: Omit<Config, 'baseURL'>
): Promise<T> {
  const { isEnabled: isDraftMode } = await draftMode();

  try {
    const { data } = await createClient(config)
      .single(singleTypeName)
      .find({
        ...options,
        status: isDraftMode ? 'draft' : 'published',
      });

    return data as T;
  } catch (error) {
    throw new StrapiError(
      `Failed to fetch single type "${singleTypeName}"`,
      singleTypeName,
      error
    );
  }
}

/**
 * Fetches a single document from a collection by documentId.
 * @throws {StrapiError} When the fetch fails
 */
export async function fetchDocument<T = API.Document>(
  collectionName: string,
  documentId: string,
  options?: API.BaseQueryParams,
  config?: Omit<Config, 'baseURL'>
): Promise<T> {
  const { isEnabled: isDraftMode } = await draftMode();

  try {
    const { data } = await createClient(config)
      .collection(collectionName)
      .findOne(documentId, {
        ...options,
        status: isDraftMode ? 'draft' : 'published',
      });

    return data as T;
  } catch (error) {
    throw new StrapiError(
      `Failed to fetch document "${documentId}" from "${collectionName}"`,
      collectionName,
      error
    );
  }
}

/**
 * Fetches a single document from a collection by slug.
 * @throws {StrapiError} When the fetch fails or document not found
 */
export async function fetchCollectionTypeBySlug<T = API.Document>(
  collectionName: string,
  slug: string,
  options?: API.BaseQueryParams,
  config?: Omit<Config, 'baseURL'>
): Promise<T | null> {
  const { isEnabled: isDraftMode } = await draftMode();

  try {
    const { data } = await createClient(config)
      .collection(collectionName)
      .find({
        ...options,
        filters: {
          slug: { $eq: slug },
          ...options?.filters,
        },
        status: isDraftMode ? 'draft' : 'published',
      });

    return (data[0] as T) ?? null;
  } catch (error) {
    throw new StrapiError(
      `Failed to fetch "${collectionName}" with slug "${slug}"`,
      collectionName,
      error
    );
  }
}

/**
 * Creates an authenticated client for user-specific requests.
 */
export function createAuthenticatedClient(jwt: string) {
  return createClient({ auth: jwt });
}

export { StrapiError };

Available Functions

fetchSingleType<T>

Fetches a single type from Strapi (e.g., homepage, settings).

export async function fetchSingleType<T = API.Document>(
  singleTypeName: string,
  options?: API.BaseQueryParams,
  config?: Omit<Config, 'baseURL'>
): Promise<T>

Example:

const landingPage = await fetchSingleType<TLandingPage>("landing-page");

fetchCollectionType<T>

Fetches a collection of documents (e.g., articles, products).

export async function fetchCollectionType<T = API.Document[]>(
  collectionName: string,
  options?: API.BaseQueryParams,
  config?: Omit<Config, 'baseURL'>
): Promise<T>

Example:

const articles = await fetchCollectionType<TArticle[]>("articles", {
  populate: "*",
  filters: { published: true },
});

fetchDocument<T>

Fetches a single document by ID from a collection.

export async function fetchDocument<T = API.Document>(
  collectionName: string,
  documentId: string,
  options?: API.BaseQueryParams,
  config?: Omit<Config, 'baseURL'>
): Promise<T>

Example:

const article = await fetchDocument<TArticle>("articles", "abc123");

fetchCollectionTypeBySlug<T>

Fetches a single document by slug from a collection.

export async function fetchCollectionTypeBySlug<T = API.Document>(
  collectionName: string,
  slug: string,
  options?: API.BaseQueryParams,
  config?: Omit<Config, 'baseURL'>
): Promise<T | null>

Example:

const article = await fetchCollectionTypeBySlug<TArticle>("articles", "my-article-slug");

createAuthenticatedClient

Creates a client with user JWT authentication.

export function createAuthenticatedClient(jwt: string) {
  return createClient({ auth: jwt });
}

Example:

const client = createAuthenticatedClient(userJwt);
const userProfile = await client.collection("profiles").findOne(userId);

Authentication

flowchart LR
    subgraph "Authentication Options"
        A[No Token] -->|Public API| B[Unauthenticated Request]
        C[STRAPI_API_TOKEN] -->|Server API Token| D[Token Auth Request]
        E[User JWT] -->|User Auth| F[JWT Auth Request]
    end
ModeUse CaseConfiguration
Public APINo STRAPI_API_TOKEN setAutomatic
API TokenServer-to-server requestsSet STRAPI_API_TOKEN env var
User JWTUser-specific requestsPass { auth: jwt } in config

Draft Mode Support

All fetch functions automatically check Next.js draft mode and adjust the request:

flowchart TD
    A[Fetch Request] --> B{Draft Mode?}
    B -->|Yes| C[status: 'draft']
    B -->|No| D[status: 'published']
    C --> E[Return Data]
    D --> E
const { isEnabled: isDraftMode } = await draftMode();

const { data } = await createClient(config)
  .single(singleTypeName)
  .find({
    ...options,
    status: isDraftMode ? 'draft' : 'published',
  });

Error Handling

Error Flow Overview

flowchart TD
    A[API Request] --> B{Success?}
    B -->|Yes| C[Return Data]
    B -->|No| D[Error Handling]
    D --> E{Error Type?}
    E -->|Network| F[Connection Error]
    E -->|404| G[Not Found]
    E -->|401/403| H[Auth Error]
    E -->|500| I[Server Error]
    F --> J[User Feedback]
    G --> J
    H --> J
    I --> J

Complete Error Flow in Next.js

sequenceDiagram
    participant User
    participant Page as Page Component
    participant DataLayer as Data Layer
    participant SDK as Strapi SDK
    participant Strapi as Strapi API
    participant ErrorBoundary as Error Boundary

    User->>Page: Visit /articles/my-post
    Page->>DataLayer: getArticleBySlug("my-post")
    DataLayer->>SDK: fetchCollectionTypeBySlug()
    SDK->>Strapi: GET /api/articles?filters[slug][$eq]=my-post

    alt Success
        Strapi-->>SDK: 200 OK + Data
        SDK-->>DataLayer: Article Data
        DataLayer-->>Page: Article Data
        Page-->>User: Render Article
    else Network Error
        Strapi-->>SDK: Connection Failed
        SDK->>SDK: throw StrapiError
        SDK-->>ErrorBoundary: Error Bubbles Up
        ErrorBoundary-->>User: "Something went wrong"
    else Not Found
        Strapi-->>SDK: 200 OK + Empty Array
        SDK-->>DataLayer: null
        DataLayer-->>Page: null
        Page->>Page: notFound()
        Page-->>User: 404 Page
    end

Layer Responsibilities

flowchart TB
    subgraph "Presentation Layer"
        Page[Page Component]
        ErrorUI[Error Boundary]
        NotFoundUI[Not Found Page]
    end

    subgraph "Data Layer"
        DataFetcher[Data Fetcher Functions]
    end

    subgraph "SDK Layer"
        SDK[Strapi SDK]
        StrapiError[StrapiError Class]
    end

    subgraph "External"
        API[Strapi API]
    end

    Page --> DataFetcher
    DataFetcher --> SDK
    SDK --> API
    SDK -.->|throws| StrapiError
    StrapiError -.->|catches| ErrorUI
    Page -.->|notFound()| NotFoundUI

    style StrapiError fill:#ff6b6b,stroke:#333
    style ErrorUI fill:#ffd93d,stroke:#333
    style NotFoundUI fill:#6bcb77,stroke:#333

StrapiError Properties

PropertyTypeDescription
namestringAlways "StrapiError"
messagestringHuman-readable error message
contentTypestringThe Strapi content type that failed
causeunknownOriginal error for debugging

Common Error Scenarios

flowchart LR
    subgraph "Error Scenarios"
        A[Network Error] -->|No connection| E1[StrapiError]
        B[404 Not Found] -->|Content missing| E2[Return null]
        C[401 Unauthorized] -->|Bad token| E3[StrapiError]
        D[500 Server Error] -->|Strapi down| E4[StrapiError]
    end
ScenarioCauseSDK BehaviorRecommended Handling
Network ErrorServer unreachableThrows StrapiErrorShow error boundary
Content Not FoundEmpty resultReturns nullCall notFound()
UnauthorizedInvalid/missing tokenThrows StrapiErrorRedirect to login
Server ErrorStrapi crashedThrows StrapiErrorShow error boundary

Error Handling Strategies

Let errors bubble up to Next.js error boundaries. This is the cleanest approach.

Data Layer (landing-page.ts):

import { fetchSingleType } from "@/lib/strapi-sdk";
import type { TLandingPage } from "@/types";

export async function getLandingPageData(): Promise<TLandingPage> {
  return fetchSingleType<TLandingPage>("landing-page");
}

Page Component (page.tsx):

import { notFound } from "next/navigation";
import { strapiApi } from "@/data/index";

export default async function Home() {
  const data = await strapiApi.landingPage.getLandingPageData();

  if (!data) notFound();

  return (
    <div className="flex min-h-screen">
      <BlockRenderer blocks={data.blocks} />
    </div>
  );
}

Global Error Boundary (global-error.tsx):

"use client";

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <html>
      <body>
        <div className="flex min-h-screen flex-col items-center justify-center">
          <h1 className="text-2xl font-bold mb-4">Something went wrong</h1>
          <p className="text-gray-600 mb-4">{error.message}</p>
          <button
            onClick={() => reset()}
            className="px-4 py-2 bg-primary text-white rounded hover:opacity-90"
          >
            Try again
          </button>
        </div>
      </body>
    </html>
  );
}

Strategy 2: Handle Errors Explicitly

For more control, catch errors at the data layer.

// src/data/articles.ts
import { fetchCollectionTypeBySlug, StrapiError } from "@/lib/strapi-sdk";
import type { TArticle } from "@/types";

type Result<T> =
  | { data: T; error: null }
  | { data: null; error: string };

export async function getArticleBySlug(slug: string): Promise<Result<TArticle>> {
  try {
    const data = await fetchCollectionTypeBySlug<TArticle>("articles", slug);

    if (!data) {
      return { data: null, error: "Article not found" };
    }

    return { data, error: null };
  } catch (err) {
    const message = err instanceof StrapiError
      ? err.message
      : "Failed to load article";

    return { data: null, error: message };
  }
}

Usage:

const { data, error } = await getArticleBySlug(slug);

if (error) {
  // Handle error
}

if (!data) {
  notFound();
}

Strategy 3: Type-Safe Error Handling with Discriminated Unions

type FetchResult<T> =
  | { status: "success"; data: T }
  | { status: "not_found" }
  | { status: "error"; error: StrapiError };

export async function getArticleBySlug(slug: string): Promise<FetchResult<TArticle>> {
  try {
    const data = await fetchCollectionTypeBySlug<TArticle>("articles", slug);

    if (!data) {
      return { status: "not_found" };
    }

    return { status: "success", data };
  } catch (err) {
    return {
      status: "error",
      error: err instanceof StrapiError ? err : new StrapiError("Unknown error", "articles", err)
    };
  }
}

// Usage with exhaustive type checking
const result = await getArticleBySlug(slug);

switch (result.status) {
  case "success":
    return <Article data={result.data} />;
  case "not_found":
    notFound();
  case "error":
    throw result.error;
}

Data Layer Pattern

We organize API calls through a centralized data layer:

flowchart LR
    subgraph "Data Layer"
        Index[index.ts]
        LP[landing-page.ts]
        Articles[articles.ts]
        Other[...]
    end

    Index --> LP
    Index --> Articles
    Index --> Other

    Page[Page Components] --> Index

Export structure (data/index.ts):

import { getLandingPageData } from "./landing-page";

export const strapiApi = {
  landingPage: {
    getLandingPageData
  }
}

Environment Variables

VariableDescriptionRequired
NEXT_PUBLIC_STRAPI_URLStrapi backend URLYes
STRAPI_API_TOKENAPI token for authenticated requestsNo

Best Practices

1. Type Safety

Always provide generic types to fetch functions:

// Good
fetchSingleType<TLandingPage>("landing-page")

// Bad
fetchSingleType("landing-page")

2. Let Errors Propagate

Don't wrap calls in try/catch unless you need custom handling.

3. Use Data Layer

Keep API calls in src/data/ for organization.

4. Draft Mode

The SDK handles draft mode automatically - no manual configuration needed.

5. Authentication

Use createAuthenticatedClient for user-specific requests.

6. Use Custom Error Classes

// Good: Custom error with context
throw new StrapiError(
  `Failed to fetch collection "${collectionName}"`,
  collectionName,
  originalError
);

// Bad: Generic error
throw new Error("API call failed");

7. Preserve Original Errors

// Good: Keep the cause for debugging
catch (error) {
  throw new StrapiError(message, contentType, error);
}

// Bad: Lose the original error
catch (error) {
  throw new StrapiError(message, contentType);
}

8. Handle Not Found vs Errors Differently

flowchart TD
    A[Fetch Result] --> B{Has Data?}
    B -->|Yes| C[Return Data]
    B -->|No - Empty Result| D[Return null / notFound]
    B -->|No - Exception| E[Throw StrapiError]

    style D fill:#6bcb77,stroke:#333
    style E fill:#ff6b6b,stroke:#333
// Not Found = expected case, return null
if (!data) return null;

// Error = unexpected case, throw
throw new StrapiError(...);

9. Log Errors in Production

catch (error) {
  // Log for monitoring
  console.error(`[Strapi] Failed to fetch ${contentType}:`, error);

  // Re-throw with context
  throw new StrapiError(message, contentType, error);
}

10. Use Type Guards

function isStrapiError(error: unknown): error is StrapiError {
  return error instanceof StrapiError;
}

// Usage
catch (error) {
  if (isStrapiError(error)) {
    console.log(`Content type: ${error.contentType}`);
  }
}

Comparison: Basic vs Advanced

FeatureBasicAdvanced
Custom Error ClassNoYes (StrapiError)
Error ContextLimitedFull (contentType, cause)
Type SafetyNoYes (generics)
Draft ModeNoYes (automatic)
Auth HandlingManualAutomatic
DebuggingHarderEasier
Code ComplexityLowMedium
Production ReadyNoYes

Summary

flowchart TB
    subgraph "Choose Your Approach"
        A[Beginner?] -->|Yes| B[Use Simple SDK]
        A -->|No| C[Use Advanced SDK]

        B --> D[Add try/catch as needed]
        C --> E[Let errors propagate to boundaries]

        D --> F[Learn patterns]
        E --> G[Production ready]

        F -->|Graduate to| C
    end

Key Takeaways:

  1. Start with the simple SDK to learn the basics
  2. Graduate to the advanced SDK for production
  3. Use custom error classes for better debugging
  4. Let errors propagate to error boundaries
  5. Handle "not found" differently from errors
  6. Always preserve the original error cause
Built with
Strapi
TanStack Start
RetroUI
View source on GitHub