Design System
Patterns & Recipes

How do I fetch data from Hygraph?

hygraphFetch / hygraphMutate, via @digital-web-platform/graphql-client.

Prescribed: hygraphFetch/hygraphMutate from @digital-web-platform/graphql-client, called from a Server Component. Don't fetch from a "use client" component, and don't hand-roll a fetch() call to the Hygraph endpoint — the shared client already handles auth, retries, and ISR revalidation consistently across apps.

This is a code pattern, not a live-preview component — hygraphFetch/hygraphMutate are server-only and need real Hygraph credentials this docs app doesn't have, so there's no interactive demo on this page. Every snippet below is read live from the actual files it names, not hand-copied, so it can't drift from the real implementation.

Reads: hygraphFetch

Query at the top of the file, call it from an async Server Component, and handle the failure case explicitly — this is the entire pattern, taken from a real (if intentionally minimal) app:

import { hygraphFetch } from "@digital-web-platform/graphql-client";
import type { PrototypeCardsResponse } from "@digital-web-platform/hygraph-schema";

const QUERY = `
  query PrototypeCards {
    prototypeCards(orderBy: sortOrder_ASC) {
      title
      description
      sortOrder
    }
  }
`;

export default async function Home() {
  let prototypeCards: PrototypeCardsResponse["prototypeCards"];

  try {
    const data = await hygraphFetch<PrototypeCardsResponse>(QUERY);
    prototypeCards = data.prototypeCards;
  } catch {
    return (
      <div
        role="alert"
        className="rounded-lg border border-red-200 bg-red-50 p-6 dark:border-red-800 dark:bg-red-950"
      >
        <h3 className="text-lg font-semibold text-red-800 dark:text-red-200">
          Failed to load prototypes
        </h3>
        <p className="mt-2 text-sm text-red-600 dark:text-red-400">
          Unable to fetch prototype cards from the CMS. Please try again later.
        </p>
      </div>
    );
  }

  return (
    <div className="flex flex-col gap-6">
      {prototypeCards.map((card) => (
        <div
          key={card.sortOrder}
          className="rounded-lg border border-zinc-200 p-6 dark:border-zinc-800"
        >
          <h3 className="text-lg font-bold text-black dark:text-zinc-50">
            {card.title}
          </h3>
          <p className="mt-2 text-sm text-zinc-500 dark:text-zinc-400">
            {card.description}
          </p>
        </div>
      ))}
    </div>
  );
}

By default, hygraphFetch sets next: { revalidate: 60 } (ISR) and retries twice on failure. A real app usually wants a named function per query, not an inline call in the component — wrap the shared client once per app and reshape the result to the app's own types, as digital-gp does:

  hours.wed.hours = g.hoursWed;
  hours.thu.hours = g.hoursThu;
  hours.fri.hours = g.hoursFri;
  hours.sat.hours = g.hoursSat;
  return hours;
}

function toBrandFilter(brand: "rm" | "cs"): StagingBrand[] {
  return brand === "rm" ? ["rm", "both"] : ["cs", "both"];
}

// ---------------------------------------------------------------------------
// Preview brand lookup
// Used by /api/draft to resolve which brand portal to open when previewing
// an individual content entry by its Hygraph ID. The dev model→typename
// mapping has changed because of the schema collapse: DashboardTask is now
// ChecklistItem, InboxMessageTemplate is now Block(blockType:inboxMessage),
// BrandConfig is now SiteSettings, WelcomePackageType is now
// Block(blockType:welcomePackage).
// ---------------------------------------------------------------------------

const PREVIEW_TYPE_MAP: Record<string, { field: string; tab: string }> = {
  ChecklistItem: { field: "checklistItem", tab: "dashboard" },
  Block: { field: "block", tab: "dashboard" },
  SiteSettings: { field: "siteSettings", tab: "dashboard" },
  GuestPortalPage: { field: "guestPortalPage", tab: "dashboard" },
  // Legacy typenames preserved as aliases so old preview links still resolve
  // to the right tab; the actual entry won't be found at the dev id, but the
  // brand+tab fallback below will still pick a sensible default.
  DashboardTask: { field: "checklistItem", tab: "dashboard" },
  InboxMessageTemplate: { field: "block", tab: "dashboard" },
  BrandConfig: { field: "siteSettings", tab: "dashboard" },
  WelcomePackageType: { field: "block", tab: "check-in" },
};

export async function getBrandAndTabForEntry(

Override the defaults at the call site when a query genuinely needs to skip the cache (as this one does, via a fixed { cache: "no-store", retries: 0 } passed through the app's own wrapper) — don't change the shared client's defaults for everyone.

Writes: hygraphMutate

No production mutation call-site exists yet anywhere in this repo — the snippet below is illustrative, shaped by the package's own contract and unit tests, not a working reference:

import { hygraphMutate } from "@digital-web-platform/graphql-client";

const MUTATION = `
  mutation UpdateChecklistItem($id: ID!, $completed: Boolean!) {
    updateChecklistItem(where: { id: $id }, data: { completed: $completed }) { id }
  }
`;

export async function POST(request: Request) {
  const { id, completed } = await request.json();
  await hygraphMutate(MUTATION, { id, completed });
  return Response.json({ ok: true });
}

Unlike hygraphFetch, hygraphMutate does not retry by default (retries: 0) — a write retried after a timeout can double-apply, so opting in is a deliberate per-call decision, not the default.

Do / Don't

  • Do call hygraphFetch/hygraphMutate from a Server Component or a route handler — never from client code. Both modules import server-only and will fail to build if you try.
  • Do give a mutation its own route handler under app/api/<feature>/route.ts — this repo's mutations go through route handlers, not Server Actions, per docs/architecture.md.
  • Don't assume retries are safe for a write the way they are for a read — pass { retries: n } explicitly only when the mutation is genuinely idempotent.

On this page