Skip to the content
Documentation
Help Start
Reading in:
  • German: 9,059 of 9,059 pages
About the help in other languages
All help pages

A front end on the GraphQL API, step by step

This guide takes an Exponential 6.0.15 site from "GraphQL switched off" to a running front end that reads its content through the GraphQL API: the API switched on for one siteaccess, a key for the front end, a starter…

Download as PDF

Address: https://doc.exponential.earth/exphelp/viewer/guides/graphql-walkthrough.md

On this page

A front end on the GraphQL API, step by step

This guide takes an Exponential 6.0.15 site from "GraphQL switched off" to a running front end that reads its content through the GraphQL API: the API switched on for one siteaccess, a key for the front end, a starter project generated from the site's own schema, its queries stored on the server and answered from the response cache, and, if the front end also writes, a key that may write (the mutations come on with the API). Every step says what to run, what you should see and how to check it.

It is written for a developer with a shell on the server (for ./console) and an administrator account. The reference for everything used here is The GraphQL API; copy-paste solutions for common front ends are in GraphQL recipes.

Time20 minutes, plus npm install
You needExponential 6.0.15, webonyx/graphql-php 15 (optional; installed by the kernel's own Composer set-up, see The library), a shell in the installation root, Node.js 20 or later for the JavaScript kits
You getan end point at https://<your site>/api/graphql, a personal API key, a Next.js, Nuxt, Astro, plain HTML or Expo project reading the site

Contents

  1. Check that the API can run
  2. See what switching it on changes
  3. Switch it on
  4. Look at Setup > GraphQL
  5. Ask the first question
  6. Generate the starter project
  7. Run it
  8. Store its queries on the server
  9. Writing: a key with graphql:write
  10. Before going live
  11. When something does not work

1. Check that the API can run

./console exp:graphql:status --siteaccess=site

The last line says PASS the GraphQL API can run (switched off here): the library is installed and the schema builds from your content classes. schema names its hash and the number of classes and fields; string only lists the datatypes that have no type of their own (they answer their text). FAIL GraphQL API unavailable: the library is missing means composer install --no-dev left it out (the library is optional), or [GraphQLSettings] LibraryPath points nowhere; the library is not webonyx/graphql-php 15 means a platform installed another version. See The library.

exp:graphql:status for a siteaccess where the API is off: the settings, the library, the schema with 88 classes, and PASS the GraphQL API can run (switched off here)
exp:graphql:status for a siteaccess where the API is off: the settings, the library, the schema with 88 classes, and PASS the GraphQL API can run (switched off here)

2. See what switching it on changes

Name the siteaccess the front end reads, the address the front end will run at during development, and the user the front end reads as. Without --apply nothing is written:

./console exp:graphql:enable --siteaccess=site --origin=http://localhost:3000 --key-for=frontend

The command first builds the schema (PASS the schema builds: 88 classes, schema 00818b7ff593), then prints, for every settings file, the diff it would write:

DRY RUN would change settings/override/graphql.ini.append.php
+[GraphQLSettings]
+Enabled=enabled
+SiteAccessList[]=site
DRY RUN would change settings/override/rest.ini.append.php
+[CorsSettings]
+Cors=enabled
+AllowedOrigins[]=http://localhost:3000
DRY RUN would make an API key for frontend with graphql:read
  • The user of the key decides what the front end can read: make a user (here frontend) in a group whose role allows content/read on what the site shows, and nothing more. The anonymous user is enough for a public site; then leave --key-for out.
  • --origin is only needed when a browser calls the API directly (the plain HTML kit, a client-side app). The Next.js, Nuxt and Astro kits call it from the server.
The dry run of exp:graphql:enable: the schema builds, graphql.ini says so already, the diff it would write to rest.ini for CORS, the API key it would make, and DRY RUN nothing written
The dry run of exp:graphql:enable: the schema builds, graphql.ini says so already, the diff it would write to rest.ini for CORS, the API key it would make, and DRY RUN nothing written

3. Switch it on

The same command with --apply:

./console exp:graphql:enable --siteaccess=site --origin=http://localhost:3000 --key-for=frontend --apply
PASS wrote settings/override/graphql.ini.append.php
PASS wrote settings/override/rest.ini.append.php
PASS API key expk_qkjdsz3uznp8 for frontend (graphql:read), shown once:

    expk_qkjdsz3uznp8_95UvceuCwfzIUjYhD8SidKTprQ756aTKC7XyjRcF

Copy the key now: it is shown this once and stored only as a hash. Each file was backed up to var/backup/ini/ first, the change is in the audit (system.setting.write, access.apikey.create), and the INI cache was cleared, so PHP-FPM and Velocity read the new settings on their next request. Check:

./console exp:graphql:status --siteaccess=site      # enabled  yes
exp:graphql:status for the siteaccess site after switching on: enabled yes, mutations yes, and PASS the GraphQL API can run
exp:graphql:status for the siteaccess site after switching on: enabled yes, mutations yes, and PASS the GraphQL API can run

4. Look at Setup > GraphQL

In the administration, Setup > GraphQL now shows the end point, the library, the schema, and a table of every siteaccess with the API on in site and off elsewhere. Below it: the limits of the siteaccess, the response cache (empty so far), the persisted queries (none yet), the API keys with a GraphQL scope (the key of step 3, active, never used), and the playground. With the policy graphql/administrate the table has the buttons Switch on, Switch off, Mutations on and Mutations off: they write the same settings as the commands.

Setup > GraphQL: the end point, the library, the table of siteaccesses with the API and its mutations on in site and off elsewhere, and the buttons Switch off, Mutations off and Switch on
Setup > GraphQL: the end point, the library, the table of siteaccesses with the API and its mutations on in site and off elsewhere, and the buttons Switch off, Mutations off and Switch on
The API keys with a GraphQL scope: the new key of frontend with graphql:read, active, next to two revoked keys
The API keys with a GraphQL scope: the new key of frontend with graphql:read, active, next to two revoked keys

The playground is shown when the API is also switched on in the siteaccess of the administration; while it is off there, a notice at the top says so and the table shows where the API answers.

Type a query into the playground and press Run (Ctrl+Enter):

{ node(urlAlias: "/") { name children(limit: 5) { totalCount nodes { name urlAlias classIdentifier } } } }

It runs with your rights, as the signed-in administrator, so it may show more than the front end will.

5. Ask the first question

From a shell, as the front end will, with the key:

curl -s https://www.example.com/api/graphql \
     -H "Authorization: Bearer $EXP_API_KEY" \
     -H 'Content-Type: application/json' \
     -d '{"query": "{ currentUser { login signedInBy } node(urlAlias: \"fit-healthy\") { name children(limit: 3) { nodes { name urlAlias } } } }"}'
{"data":{"currentUser":{"login":"frontend","signedInBy":"apikey"},"node":{"name":"Fit & Healthy","children":{"nodes":[{"name":"Fitness","urlAlias":"fit-healthy/fitness"},{"name":"Healthy eating","urlAlias":"fit-healthy/healthy-eating"},{"name":"Recipes","urlAlias":"fit-healthy/recipes"}]}}}}

signedInBy is apikey: the request ran as the key's owner. Without the header it runs as the anonymous user (anonymous), and the schema (introspection) is closed to it.

A query sent with curl and the key: currentUser answers frontend, signed in by apikey, and the page Fit & Healthy with its first three children
A query sent with curl and the key: currentUser answers frontend, signed in by apikey, and the page Fit & Healthy with its first three children

6. Generate the starter project

./console exp:graphql:starter --list
./console exp:graphql:starter --kit=next --siteaccess=site --to=../shop-front   # what it would write
./console exp:graphql:starter --kit=next --siteaccess=site --to=../shop-front --apply
exp:graphql:starter --list with the kits html, next, nuxt, astro and expo, then the dry run of the kit next: the files it would write (the queries per content class shortened), and DRY RUN 99 files of the kit next, run again with --apply
exp:graphql:starter --list with the kits html, next, nuxt, astro and expo, then the dry run of the kit next: the files it would write (the queries per content class shortened), and DRY RUN 99 files of the kit next, run again with --apply
KitForRuns the queries
htmla page without a build stepin the browser (needs --origin of step 2)
nextNext.js 15, App Routeron the server (React server components)
nuxtNuxt 3on the server (the app's own /api/graphql route adds the key)
astroa static siteat build time
expoan iOS, Android and web app (React Native)in the app

Before anything is written, every query the kit gets is checked against the schema and its limits. The project has:

  • the kit's files (shipped in share/graphql/starters/<kit>/), with the end point filled in (--endpoint= to name another address than site.ini [SiteSettings] SiteURL);
  • the generated queries: Menu (the children of the site's root), Node (a page by its URL alias: name, class, parent, children) and one query per content class (Content_ng_article ...) that reads every field of the class: images in the alias large with their alternative text, rich text as HTML, relations by name and URL alias. They are in the kit's module (lib/queries.ts for Next.js) and in graphql/*.graphql;
  • a README with the next steps.

The queries follow the schema of the moment. After a content class changed (exp:graphql:status shows a new schema hash), generate the kit again into a new directory and copy lib/queries.ts and graphql/ over.

7. Run it

The --apply of step 6 writes the project and ends with the commands that start it:

exp:graphql:starter --apply: PASS for every file written to ../shop-front (the queries per content class shortened), and PASS 99 files of the kit next, Next: cd ../shop-front && npm install && npm run dev
exp:graphql:starter --apply: PASS for every file written to ../shop-front (the queries per content class shortened), and PASS 99 files of the kit next, Next: cd ../shop-front && npm install && npm run dev
cd ../shop-front
echo "EXP_GRAPHQL_KEY=$EXP_API_KEY" >> .env.local
npm install
npm run dev                  # http://localhost:3000/

The home page shows the menu of the site's root; every address of the site is a page (/fit-healthy/fitness is the category, /fit-healthy/fitness/why-track-your-workouts the article) with the fields of its class and its children. Change components/Field.tsx and app/[[...path]]/page.tsx to design your pages: the data is already there.

8. Store its queries on the server

A stored (persisted) query is called by its name instead of its text, cannot be changed by whoever calls it, and is the only kind the response cache keeps:

cd -                                                   # back to the installation
./console exp:graphql:persist ../shop-front/graphql/ --siteaccess=site           # check: 90 queries fit the schema
./console exp:graphql:persist ../shop-front/graphql/ --siteaccess=site --apply   # store them
curl -s 'https://www.example.com/api/graphql?id=menu'                 # a GET by name
curl -sI 'https://www.example.com/api/graphql?id=menu' | grep -i x-graphql-cache    # MISS, then HIT

The second call is answered from the cache, at the start of index_rest.php, in about a millisecond; publishing content expires the cache. With PersistedQueriesOnly=anonymous the site then refuses every ad-hoc query of anonymous requests: a good setting for production once the front end only sends stored queries. Setup > GraphQL lists the stored queries and the size of the cache.

The check of exp:graphql:persist without --apply: the SHA-256 and the name each query would be stored under, menu and node among them, and DRY RUN 90 queries fit the schema
The check of exp:graphql:persist without --apply: the SHA-256 and the name each query would be stored under, menu and node among them, and DRY RUN 90 queries fit the schema
The persisted queries on Setup > GraphQL: the name, the SHA-256, the size, where it is stored, and Remove
The persisted queries on Setup > GraphQL: the name, the SHA-256, the size, where it is stored, and Remove

9. Writing: a key with graphql:write

The mutations came on with the API in step 3 (MutationsEnabled=enabled is the default), but nothing can write yet: a mutation needs a signed-in user, and a key needs the scope graphql:write. When the front end changes content (a contact form that creates content, an editor app), give the writing user such a key (--mutations also switches the mutations on again, should they have been switched off for the siteaccess):

./console exp:graphql:enable --siteaccess=site --mutations --key-for=editor --apply
The dry run of exp:graphql:enable --mutations: the diff that switches the mutations on for site, and the key it would make for editor with graphql:read graphql:write
The dry run of exp:graphql:enable --mutations: the diff that switches the mutations on for site, and the key it would make for editor with graphql:read graphql:write

The key of editor gets graphql:read graphql:write, and still only does what the roles of editor allow.

curl -s https://www.example.com/api/graphql -H "Authorization: Bearer $EDITOR_KEY" -H 'Content-Type: application/json' \
     -d '{"query": "mutation { createContent(parentId: 2, classIdentifier: \"folder\", fields: [{identifier: \"name\", value: \"From the front end\"}], publish: false) { status contentId version } }"}'
{"data":{"createContent":{"status":"DRAFT","contentId":59338,"version":1}}}

Every mutation is recorded in the audit (content.graphql.mutation), runs the workflows of the kernel's operations, and expires the response cache. Files go as multipart requests (uploadImage), and a draft can be shown before it is published with a preview token. See Mutations.

Never put a key with graphql:write into a browser or an app that others install: keep it on a server.

10. Before going live

  • To do: --origin lists the production address of a browser front end, and not http://localhost:3000 any more (settings/override/rest.ini.append.php, AllowedOrigins[]).
  • To do: The key's user reads only what the site shows; a browser or app gets no key, or one with graphql:read only.
  • To do: The front end's queries are stored (exp:graphql:persist), and PersistedQueriesOnly=anonymous (or enabled).
  • To do: The rate limits fit the expected traffic (RateLimit, RateLimitAnonymous; Setup > GraphQL shows them).
  • To do: A siteaccess where nothing writes has MutationsEnabled=disabled (exp:graphql:disable --siteaccess=<name> --mutations --apply), and exp:graphql:keys lists no key you do not know.
  • To do: Debug=disabled on the production siteaccess.
The limits and the response cache on Setup > GraphQL: depth, complexity, items per list, the rate limits, introspection, persisted queries only, the largest query and upload, and the state of the cache
The limits and the response cache on Setup > GraphQL: depth, complexity, items per list, the rate limits, introspection, persisted queries only, the largest query and upload, and the state of the cache

11. When something does not work

You seeBecauseDo
404 and DISABLEDthe API is off in the siteaccess the request reachedexp:graphql:status --siteaccess=<it>; a request reaches a siteaccess by host, path or the header X-Siteaccess
403 insufficient_scopethe key has no graphql:readmake a key with exp:graphql:keys --create --for=<login> --apply
null for content you know is therethe user may not read itcheck the role of the key's user, or try the query in the playground as that user's role
a browser error about CORSthe front end's origin is not allowedexp:graphql:enable --origin=<the address> --apply
429 and RATE_LIMITEDover the rate limitwait for Retry-After, or raise the limit for the siteaccess
FORBIDDEN on a mutationsigned out, no graphql:write, or the user's policiesthe error message names which

The error codes are listed under Errors.

A request that reaches a siteaccess where the API is off: the error DISABLED, The GraphQL API is switched off in this siteaccess, and HTTP 404
A request that reaches a siteaccess where the API is off: the error DISABLED, The GraphQL API is switched off in this siteaccess, and HTTP 404

Something wrong on this page? Report an error

Documentation license

This documentation is free documentation, licensed under the GNU Free Documentation License 1.3 or later.

Exponential Copyright © 1998-2026 Exponential Foundation and 7x