Craft CMS GraphQL API Setup
Craft CMS 3.3+ includes a built-in GraphQL API. Available by default at /api or custom endpoint. Supports querying elements (Entry, Asset, Category, Tag, User), but not mutations (changing data via GraphQL is not natively supported).
Enabling and configuration
// config/general.php
'enableGraphqlApi' => true,
'maxGraphqlComplexity' => 500, // query complexity limit
'maxGraphqlDepth' => 10, // maximum nesting depth
'maxGraphqlResults' => 100, // maximum result count
Access tokens
In CP → GraphQL → Schemas create a schema with needed permissions:
- Public Schema — no authorization, public content only
- Private Schema — with token, can include drafts
// Request with token
fetch('/api', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.CRAFT_GRAPHQL_TOKEN}`,
},
body: JSON.stringify({ query, variables }),
});
Typical queries
# List of blog posts
query BlogPosts($limit: Int, $offset: Int) {
entries(
section: "blog"
orderBy: "postDate DESC"
limit: $limit
offset: $offset
status: "live"
) {
id
title
slug
postDate @formatDateTime(format: "d.m.Y")
url
... on blog_article_Entry {
summary
heroImage { url(width: 800) alt width height }
categories { title slug }
author { fullName photo { url(width: 100, height: 100) } }
}
}
entryCount(section: "blog", status: "live")
}
# Single post by slug
query BlogPost($slug: String!) {
entry(section: "blog", slug: [$slug]) {
... on blog_article_Entry {
title
postDate @formatDateTime(format: "d MMMM Y", locale: "ru")
pageBody {
... on pageBody_richText_BlockType {
typeHandle
content
}
... on pageBody_image_BlockType {
typeHandle
image { url(width: 1200) alt width height }
caption
alignment
}
... on pageBody_codeBlock_BlockType {
typeHandle
language
code
}
}
}
}
}
Next.js client with caching
// lib/craftGraphql.ts
const CRAFT_ENDPOINT = process.env.CRAFT_GRAPHQL_URL!;
const CRAFT_TOKEN = process.env.CRAFT_GRAPHQL_TOKEN!;
async function craftQuery<T>(
query: string,
variables?: Record<string, unknown>,
options?: { revalidate?: number }
): Promise<T> {
const res = await fetch(CRAFT_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CRAFT_TOKEN}`,
},
body: JSON.stringify({ query, variables }),
next: { revalidate: options?.revalidate ?? 3600 },
});
const { data, errors } = await res.json();
if (errors?.length) throw new Error(errors[0].message);
return data;
}
Inline Fragments for Entry Types
Each Entry Type is available as a separate type in GraphQL via {sectionHandle}_{typeHandle}_Entry pattern:
entries(section: ["blog", "news"]) {
__typename
id
title
... on blog_article_Entry {
summary
readingTime
}
... on blog_podcast_Entry {
audioFile { url }
duration
}
... on news_pressRelease_Entry {
pdfFile { url }
companyName
}
}
Custom GraphQL queries via plugin/module
Built-in GraphQL only reads data. For mutations use custom REST endpoint:
// modules/sitecustom/controllers/ApiController.php
public function actionSubmitForm(): Response
{
\Craft::$app->response->headers->add('Content-Type', 'application/json');
$data = \Craft::$app->request->post();
// Validation, create element via Craft API
$entry = new Entry();
$entry->sectionId = \Craft::$app->sections->getSectionByHandle('submissions')->id;
$entry->setFieldValues(['name' => $data['name'], 'email' => $data['email']]);
if (\Craft::$app->elements->saveElement($entry)) {
return $this->asJson(['success' => true]);
}
return $this->asJson(['errors' => $entry->errors], 422);
}
Setting up GraphQL API with tokens and Next.js integration — 1–2 days.







