# Canvas Data Fetching

> Fetch and render Drupal content in Canvas components with JSON:API and SWR patterns. Use when building content lists, integrating with SWR, querying related entities, or constructing/changing any JSON:API request — every generated request must be executed and verified to return the expected results before rendering logic is written against it. Covers JsonApiClient, DrupalJsonApiParams, relationship handling, filter patterns, and request verification.

- Skill: `drupal-canvas/canvas-data-fetching` (Agent Skill)
- Install (CLI): `npx skillmds@latest add drupal-canvas/canvas-data-fetching`
- Raw SKILL.md: https://api.skillmd.com/api/skills/drupal-canvas/canvas-data-fetching/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: drupal-canvas (https://skillmd.com/u/drupal-canvas)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/drupal-canvas/canvas-data-fetching

---


# Data fetching

## Project type

Before applying this skill, check `package.json` for a dependency named
`@drupal-canvas/headless` or starting with `@drupal-canvas/headless-`. If one is
present, this is a Canvas Headless codebase — read
[`canvas-headless`](../canvas-headless/SKILL.md) first. In headless projects the
`drupal-canvas` package (including its `JsonApiClient`) is not supported: fetch
page trees with the SDK's `fetchPage()` and content with the SDK's JSON:API
client (`getClient()`), using the framework's idiomatic data-loading path. Do
not copy the SWR + `JsonApiClient` patterns below into a headless codebase. The
content-modeling workflow in this skill (identifying content types, verifying
queries, checking deserialized shapes) still applies conceptually. If no such
dependency is present, this is a Canvas-rendered React codebase: components are
React (`index.jsx`/`.tsx`) and everything in this skill applies as written.
These are the only two project types.

## Data fetching with SWR

Use [SWR](https://swr.vercel.app/) for all data fetching. It provides caching,
revalidation, and a clean hook-based API.

```jsx
import useSWR from 'swr';

const fetcher = (url) => fetch(url).then((res) => res.json());

export default function Profile() {
  const { data, error, isLoading } = useSWR(
    'https://my-site.com/api/user',
    fetcher,
  );

  if (error) return <div>Failed to load</div>;
  if (isLoading) return <div>Loading...</div>;
  return <div>Hello, {data.name}!</div>;
}
```

## Fetching Drupal content with JSON:API

To fetch content from Drupal (e.g., articles, events, or other content types),
use the autoconfigured `JsonApiClient` from the `drupal-canvas` package combined
with `DrupalJsonApiParams` for query building.

**Important:** Keep the default serializer enabled in final component code. The
runtime contract for Canvas components is the deserialized shape returned by
`JsonApiClient`, not the raw JSON:API document shape.

**Important:** Do not fabricate JSON:API resource payloads in Workbench mocks.
Components that fetch data should render their real loading, empty, or error
states in Workbench unless the user explicitly asks for a static, non-fetching
preview shape.

### Raw JSON:API vs deserialized Canvas data

Do not write component logic from raw JSON:API assumptions such as
`data[0].attributes.title` or `data[0].relationships.field_image`. Components
using `JsonApiClient` receive deserialized objects instead.

- Use plain HTTP requests only for connectivity checks and broad endpoint
  existence.
- Use `JsonApiClient` to inspect the actual shape your component will consume.
- If you inspect the raw JSON:API document for debugging, treat it as a
  secondary diagnostic view, not the source of truth for component code.
- Do not disable the serializer in final component code.

### Verify every JSON:API request returns the expected results

Any JSON:API request you generate — for a new component, a refactor, a filter
change, an added include, a changed sort, or a new query for an existing
component — must be **executed and verified** before any rendering logic is
written or changed against it. Do not assume a query is correct because it
"looks right". Build the query, run it, and confirm the response matches
expectations.

A request is verified only after **all** of these checks pass:

- **It runs.** No HTTP error, no JSON:API error document, no client exception.
- **The result count matches expectations.** A list query should return a
  non-empty collection when content of that type exists. A filtered query should
  return fewer items than the unfiltered query (and zero only when zero is
  genuinely expected). A single-resource fetch should return one resource, not
  `null`.
- **The expected fields are present and populated** on the deserialized objects
  — including fields requested via `addFields`. Missing or consistently `null`
  fields mean the query, the field name, or the content type is wrong.
- **Includes resolved** to real related entities, not bare references. If you
  used `addInclude`, confirm the relationship is hydrated on the deserialized
  object the component will read.
- **Filters and sorts behave as intended.** Spot-check that filtered items
  actually match the filter criteria and sorted items are in the requested
  order.

If any check fails, **fix the query, the field names, or the content-type
assumptions — not the component**. Do not paper over an empty or wrong response
with optional chaining, fallback strings, or "looks fine in the UI" reasoning.
Re-run the probe after each fix and only proceed once the response matches
expectations.

Use the probe pattern in the next section as the default mechanism for these
checks. A probe that prints `count: 0`, `keys: []`, or a shape missing the
fields the component needs is a failed verification, not a green light.

### Probe the deserialized shape before coding

Before writing rendering logic, run a one-off JavaScript probe that uses the
same `JsonApiClient` call and `DrupalJsonApiParams` query pattern the component
will use. Inspect the first returned item and write the component against that
deserialized shape.

This probe runs outside the Canvas runtime, so it must provide `baseUrl` and
`apiPrefix` explicitly. Final component code should not copy that setup;
Canvas-provided component code should use the normal autoconfigured
`new JsonApiClient()` path instead.

Use a command in this pattern:

```bash
node --input-type=module -e "
globalThis.window = {};
import { JsonApiClient } from 'drupal-canvas';
import { DrupalJsonApiParams } from 'drupal-jsonapi-params';

const describeShape = (value) => {
  if (Array.isArray(value)) {
    return value.length > 0 ? [describeShape(value[0])] : ['empty-array'];
  }
  if (value && typeof value === 'object') {
    return Object.fromEntries(
      Object.entries(value).map(([key, nestedValue]) => [key, describeShape(nestedValue)]),
    );
  }
  if (value === null) {
    return 'null';
  }
  return typeof value;
};

const client = new JsonApiClient('https://example.ddev.site', {
  apiPrefix: 'jsonapi',
});
const queryString = new DrupalJsonApiParams()
  .addSort('created', 'DESC')
  .addFields('node--article', ['title', 'created', 'body', 'path'])
  .getQueryString();

const items = await client.getCollection('node--article', { queryString });
const first = items?.[0];

console.log('count:', items?.length ?? 0);
console.log('keys:', first ? Object.keys(first) : []);
console.log('shape:', JSON.stringify(first ? describeShape(first) : null, null, 2));
console.log(JSON.stringify(first, null, 2));
"
```

Pass the site root as `baseUrl`, not the `/jsonapi` endpoint. Adjust the
resource type, filters, includes, sorts, and fields to match the component you
are building. Do not inspect one query shape and implement a different one in
the component.

If this probe fails in a local HTTPS development environment, check whether Node
trusts the local certificate chain before assuming the JSON:API client or query
is wrong.

```jsx
import { getNodePath, JsonApiClient } from 'drupal-canvas';
import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
import useSWR from 'swr';

const Articles = () => {
  const client = new JsonApiClient();
  const { data, error, isLoading } = useSWR(
    [
      'node--article',
      {
        queryString: new DrupalJsonApiParams()
          .addSort('created', 'DESC')
          .getQueryString(),
      },
    ],
    ([type, options]) => client.getCollection(type, options),
  );

  if (error) return 'An error has occurred.';
  if (isLoading) return 'Loading...';
  return (
    <ul>
      {data.map((article) => (
        <li key={article.id}>
          <a href={getNodePath(article)}>{article.title}</a>
        </li>
      ))}
    </ul>
  );
};

export default Articles;
```

### Including relationships with `addInclude`

When you need related entities (e.g., images, taxonomy terms), use `addInclude`
to fetch them in a single request.

**Avoid circular references in JSON:API responses.** SWR uses deep equality
checks to compare cached data, which fails with "too much recursion" errors when
the response contains circular references.

**Do not include self-referential fields.** Fields that reference the same
entity type being queried (e.g., `field_related_articles` on an article query)
create circular references: Article A references Article B, which references
back to Article A. If you need related content of the same type, fetch it in a
separate query.

**Use `addFields` to limit the response.** Always specify only the fields you
need. This improves performance and helps avoid circular reference issues:

```jsx
const params = new DrupalJsonApiParams();
params.addSort('created', 'DESC');
params.addInclude(['field_category', 'field_image']);

// Limit fields for each entity type
params.addFields('node--article', [
  'title',
  'created',
  'field_category',
  'field_image',
]);
params.addFields('taxonomy_term--categories', ['name']);
params.addFields('file--file', ['uri', 'url']);
```

## Creating content list components

When building a component that displays a list of content items (e.g., a news
listing, event calendar, or resource library), follow this workflow:

### Setup gate

Before any JSON:API discovery or content-type checks, verify local setup:

1. Resolve Canvas config values before writing code or probing Drupal. Check, in
   this order:
   - shell environment variables
   - `.env` in the project root
   - `~/.canvasrc`
2. Determine the effective `CANVAS_SITE_URL`.
3. Determine the effective `CANVAS_JSONAPI_PREFIX`. If it is not set, default to
   `jsonapi`.
4. Record the resolved values before continuing:
   - `CANVAS_SITE_URL=<resolved site root>`
   - `CANVAS_JSONAPI_PREFIX=<resolved prefix>`
5. Verify that `CANVAS_SITE_URL` is the site root, not the JSON:API endpoint.
   For example, use `https://example.ddev.site`, not
   `https://example.ddev.site/jsonapi`.
6. Send an HTTP request to `{CANVAS_SITE_URL}/{resolved JSON:API prefix}`.
   Success means HTTP `200`.
7. If the request is successful, continue with Drupal data fetching.
8. If the request is unsuccessful (or required values are missing), ask the user
   whether they want to:
   - Configure Drupal connectivity now, or
   - Continue with static content instead of Drupal fetching.
9. If the user chooses to configure connectivity, provide setup instructions:
   - `CANVAS_SITE_URL=<their Drupal site root>`
   - `CANVAS_JSONAPI_PREFIX=jsonapi` (optional; defaults to `jsonapi`) Then wait
     for the user to confirm they updated shell env, `.env`, or `~/.canvasrc`,
     and resolve the values again before retrying the request.
10. If the user chooses not to configure connectivity, proceed with static
    content.
11. Do not start content-type discovery, field inspection, or component coding
    until the effective `CANVAS_SITE_URL` and JSON:API prefix are known.
12. Do not update Vite config (`vite.config.*`) to troubleshoot connectivity.
    Connectivity issues must be resolved via correct config values and Drupal
    site availability, not build tooling changes.

### Step 1: Analyze the list structure

Examine the design to understand what data each list item needs:

- What fields are displayed (title, date, image, category, etc.)?
- How are items sorted (newest first, alphabetical, etc.)?
- Are there filters or pagination?

### Step 2: Identify or request the content type

Before writing code, verify that an appropriate content type exists in Drupal:

1. Check the JSON:API endpoint of your local Drupal site (configured via the
   resolved `CANVAS_SITE_URL` and JSON:API prefix from the Setup gate) to find a
   content type that matches the required structure. A plain HTTP request is
   acceptable for endpoint discovery only, after passing the Setup gate.

2. If a matching content type exists, use it and note which fields are
   available.

3. Inspect a sample response through `JsonApiClient` using the same resource
   type and query pattern the component will use. Run a one-off probe command,
   inspect the first returned item, and verify the deserialized field shape
   before writing rendering logic.

4. If no matching content type exists, **stop and prompt the user** to create
   one. Provide:
   - A suggested content type name
   - The required field structure based on the list design

### Step 3: Build the component

Create the content list component using JSON:API to fetch content. Only use
fields that actually exist on the content type and on the deserialized objects
returned by `JsonApiClient`—do not assume raw JSON:API field nesting will match
the runtime data shape.

### Handling filters

If the list includes filters based on entity reference fields (e.g., filter by
category, filter by author):

- **Do not hardcode filter options.** Filter options should be fetched
  dynamically using JSON:API.
- Fetch the available options for each filter (e.g., all taxonomy terms in a
  vocabulary) and populate the filter UI from that data.

This ensures filters stay in sync with the actual content in Drupal and new
options appear automatically without code changes.

## Navigation / Menu Components

Components like headers, footers, and sidebars often need menu links from
Drupal. Use a **dual implementation**: fetch from a Drupal menu when one exists,
and fall back to a static array when no menu is configured yet.

This means the component works immediately (using the hardcoded fallback), and
automatically upgrades to live Drupal-managed links once the CMS editor creates
the corresponding menu.

```jsx
import { JsonApiClient, sortMenu } from 'drupal-canvas';
import useSWR from 'swr';

// Static fallback — always define this; it renders when no Drupal menu exists
const FALLBACK_LINKS = [
  { id: 'home', title: 'Home', url: '/' },
  { id: 'about', title: 'About', url: '/about' },
];

const client = new JsonApiClient();

const Navigation = ({ menuName = 'main' }) => {
  const { data, error, isLoading } = useSWR(
    menuName ? ['menu_items', menuName] : null,
    ([type, id]) => client.getResource(type, id),
  );

  // Use live Drupal menu links when available; otherwise use fallback
  const links =
    !error && !isLoading && data ? Array.from(sortMenu(data)) : FALLBACK_LINKS;

  return (
    <nav>
      {links.map(({ id, title, url }) => (
        <a key={id} href={url}>
          {title}
        </a>
      ))}
    </nav>
  );
};
```

**Rules for menu components:**

- Always define a `FALLBACK_LINKS` constant with representative links. This
  makes the component useful in Workbench and on sites where the Drupal menu
  hasn't been created yet.
- Expose `menuName` as a prop and register it in `component.yml` so CMS editors
  can configure which Drupal menu to use without code changes.
- `menuName = null` disables fetching (SWR key is `null`) and renders the
  fallback — useful for pure static previews.
- After building a nav-type component, include a note in the manual steps
  summary telling the user to create the corresponding menu in Drupal at
  `/admin/structure/menu/add`.

**`component.yml` example for `menuName`:**

```yaml
props:
  properties:
    menuName:
      title: Menu name
      type: string
      examples:
        - main
        - footer
```

