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
- File Structure
- Basic Implementation
- Advanced Implementation
- Available Functions
- Authentication
- Draft Mode Support
- Error Handling
- Error Handling Strategies
- Data Layer Pattern
- Environment Variables
- Best Practices
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
| Mode | Use Case | Configuration |
|---|---|---|
| Public API | No STRAPI_API_TOKEN set | Automatic |
| API Token | Server-to-server requests | Set STRAPI_API_TOKEN env var |
| User JWT | User-specific requests | Pass { 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
| Property | Type | Description |
|---|---|---|
name | string | Always "StrapiError" |
message | string | Human-readable error message |
contentType | string | The Strapi content type that failed |
cause | unknown | Original 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
| Scenario | Cause | SDK Behavior | Recommended Handling |
|---|---|---|---|
| Network Error | Server unreachable | Throws StrapiError | Show error boundary |
| Content Not Found | Empty result | Returns null | Call notFound() |
| Unauthorized | Invalid/missing token | Throws StrapiError | Redirect to login |
| Server Error | Strapi crashed | Throws StrapiError | Show error boundary |
Error Handling Strategies
Strategy 1: Let Errors Propagate (Recommended)
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
| Variable | Description | Required |
|---|---|---|
NEXT_PUBLIC_STRAPI_URL | Strapi backend URL | Yes |
STRAPI_API_TOKEN | API token for authenticated requests | No |
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
| Feature | Basic | Advanced |
|---|---|---|
| Custom Error Class | No | Yes (StrapiError) |
| Error Context | Limited | Full (contentType, cause) |
| Type Safety | No | Yes (generics) |
| Draft Mode | No | Yes (automatic) |
| Auth Handling | Manual | Automatic |
| Debugging | Harder | Easier |
| Code Complexity | Low | Medium |
| Production Ready | No | Yes |
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:
- Start with the simple SDK to learn the basics
- Graduate to the advanced SDK for production
- Use custom error classes for better debugging
- Let errors propagate to error boundaries
- Handle "not found" differently from errors
- Always preserve the original error cause
