How-to library · Developer docs

How-to library

Task-shaped pages: a title that starts with a verb, the situation in a few lines, the steps, the working code in both engines, and why this way rather than another. Found from a search, done, and left.

Fetch three entity types in one request with GraphQL

A headless home page needs its navigation pages, the latest posts and the header menu. One POST to /cdna-api/graphql from your server returns all three with only the fields you render; this wraps the endpoint's rules in one function.

Context

Context. A headless home page needs its navigation pages, the latest posts and the header menu. Through REST that is three requests; through POST /cdna-api/graphql it is one, with only the fields the page renders. The call is made from your front end's server, with an API key, because the platform sends no cross-origin headers and a key must never reach a browser. The endpoint reads only, and has its own rules: the text must start with query, fields on one line are separated by commas, and there are no variables, so values are written into the query text. This how-to wraps all of that in one function.

Steps

  1. Create a backend user for the front end that can read only what the site publishes, and generate an API key on it. Keep the key in the server's environment.
  2. Write the query with an alias on each part and a first on each part.
  3. Send it as JSON, {"query": "…"}, with the key in Authorization: Bearer.
  4. Check result, not only the HTTP status, and check each record for the "Error: Getter for property …" string, which is how a misspelt field comes back.
  5. Cache the parsed result in your application for a short time, since API responses are not cached for you.

Code

JavaScript
// home.js: runs on the front end's server (Node 18 or later)
const SITE = process.env.COREDNA_URL;      // https://www.example.com
const KEY  = process.env.COREDNA_API_KEY;  // never shipped to the browser

const HOME_QUERY = `query {
  sections: Page(filter: {parent.id: 1}, orderBy: {position: "ASC"}, first: 6) {
    id, name, fullPath
  }
  latest: BlogPost(filter: {blog.id: 1}, first: 3, orderBy: {published: "DESC"}) {
    id, name, fullPath, published(format: "Y-m-d")
  }
  nav: MenuElement(menu: 1, filter: {parent: null}, orderBy: {position: "ASC"}, first: 10) {
    id, name, url
  }
}`;

let cached = null;
let cachedAt = 0;

export async function homeContent() {
  if (cached && Date.now() - cachedAt < 60_000) return cached;

  const response = await fetch(SITE + '/cdna-api/graphql', {
    method: 'POST',
    headers: { Authorization: 'Bearer ' + KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ query: HOME_QUERY }),
  });
  const body = await response.json();
  if (!response.ok || body.result !== 'success') {
    throw new Error('GraphQL failed: ' + response.status + ' ' + (body.message || ''));
  }
  for (const [part, records] of Object.entries(body.data)) {
    const bad = records.find(r => Object.values(r).some(v => typeof v === 'string' && v.startsWith('Error: Getter')));
    if (bad) throw new Error('Unknown field in part "' + part + '": ' + JSON.stringify(bad));
  }

  cached = body.data;      // { sections: [...], latest: [...], nav: [...] }
  cachedAt = Date.now();
  return cached;
}

The same request from a shell, for checking a site's data before writing any code:

Shell
curl "$COREDNA_URL/cdna-api/graphql" \
  --header "Authorization: Bearer $COREDNA_API_KEY" \
  --header 'Content-Type: application/json' \
  --data '{"query": "query { sections: Page(filter: {parent.id: 1}, first: 6) { id, name, fullPath }, latest: BlogPost(first: 3, orderBy: {published: \"DESC\"}) { id, name, fullPath }, nav: MenuElement(menu: 1, filter: {parent: null}) { id, name, url } }"}'

Why this and not that

Not three REST calls in parallel: they work, and they are the right answer if you already have a REST client, but they are three authenticated requests and three responses to merge where one does. Not a GraphQL client library: most send variables and operationName alongside query, and this endpoint refuses any key but query. Not from the browser: the response cannot be read across origins, and the key would be public. Not without first: a part with no limit returns every matching record, which is harmless with twelve posts and a timeout with twelve thousand. The ids in the query (the home page is 1, the blog 1, the header menu 1) are this example site's; read yours once with REST and put them in configuration, not in the query.

Go deeper

Was this page helpful?

Your answer goes to the docs team.