With the API and a query in hand, a page fetches its data on the server and renders the result. The example does this with one helper built on createFontdueFetch from fontdue-js/server, and a codegen step that gives every query a TypeScript type – so the data flows typed from the query all the way into your components.
Where queries live
Queries are .graphql files, one operation per file – Index.graphql, Font.graphql, Article.graphql, and so on. Keeping each query in its own file is what lets codegen read them to generate types, and lets the helper load them by name.
Fields shared across queries live beside them in their own <Name>.fragment.graphql files, pulled into the queries that use them with an #import comment – see Share GraphQL fragments across queries. The fetch helper inlines each #import before the query is sent.
Each query file is self-contained – shared fields are written inline rather than factored into fragments – because the query text is imported straight into your code with Vite’s ?raw suffix and sent as-is, with no import-resolution step.
The fetch helper
fetchGraphql reads a query file by name, posts it to your endpoint, and returns the typed data. It builds on createFontdueFetch, which resolves your store URL from the environment, unwraps the response, and throws FontdueNotFoundError when the store isn’t found (404) – the helper turns that into a Next.js notFound():
createFontdueFetch is the whole helper. Call it once at module scope to get a fetchGraphql function that resolves your store URL from the environment, posts a query, unwraps the response, and throws FontdueNotFoundError when the store isn’t found (404):
// src/lib/graphql.ts
import path from "path";
import { notFound } from "next/navigation";
import { createFontdueFetch, FontdueNotFoundError } from "fontdue-js/server";
import { processImport } from "@graphql-tools/import";
import { print } from "graphql";
const getStaticQuery = async (queryName: string) =>
print(processImport(path.resolve(process.cwd(), "src", "queries", queryName), process.cwd()));
export async function fetchGraphql<Q, V = void>(queryName: string, variables?: V): Promise<Q> {
const query = await getStaticQuery(queryName);
const fetchFontdue = createFontdueFetch();
try {
return await fetchFontdue<Q, V>(queryName, query, variables);
} catch (error) {
if (error instanceof FontdueNotFoundError) notFound();
throw error;
}
}
// src/lib/graphql.ts
import { createFontdueFetch, FontdueNotFoundError } from "fontdue-js/server";
export const fetchGraphql = createFontdueFetch();
export { FontdueNotFoundError };
// app/lib/graphql.ts
import { createFontdueFetch, FontdueNotFoundError } from "fontdue-js/server";
export const fetchGraphql = createFontdueFetch();
export { FontdueNotFoundError };
// src/lib/graphql.ts
import { createFontdueFetch, FontdueNotFoundError } from "fontdue-js/server";
export const fetchGraphql = createFontdueFetch();
export { FontdueNotFoundError };
The fetcher takes the query name, the query text, and any variables. You pass the text yourself, imported from the .graphql file with ?raw, so there’s no filesystem read or fragment resolution at request time.11The query name is only used to annotate the request URL (?query=Index), which keeps requests legible in logs; the operation itself rides in the request body.
Fetch in a page
Because the server runs the fetch before anything renders, the data is on hand when the page builds its HTML – only the result reaches the browser:
// src/app/page.tsx
import { fetchGraphql } from "@/lib/graphql";
import { IndexQuery } from "@graphql";
export default async function Home() {
const data = await fetchGraphql<IndexQuery>("Index.graphql");
// render data...
}
// app/routes/home.tsx
import { fetchGraphql } from "../lib/graphql";
import IndexDoc from "../queries/Index.graphql?raw";
import type { IndexQuery } from "../queries/operations-types";
export async function loader() {
return { data: await fetchGraphql<IndexQuery>("Index", IndexDoc) };
}
// src/routes/index.tsx
import { fetchGraphql } from "../lib/graphql";
import IndexDoc from "../queries/Index.graphql?raw";
import type { IndexQuery } from "../queries/operations-types";
export const Route = createFileRoute("/")({
loader: async () => ({ data: await fetchGraphql<IndexQuery>("Index", IndexDoc) }),
component: Home,
});
---
// src/pages/index.astro
import { fetchGraphql } from "../lib/graphql";
import IndexDoc from "../queries/Index.graphql?raw";
import type { IndexQuery } from "../queries/operations-types";
const data = await fetchGraphql<IndexQuery>("Index", IndexDoc);
---
The matching query file asks for the catalog’s root collections:
# Index.graphql
query Index {
viewer {
fontCollections(onlyRoots: true, first: 99) {
edges {
node {
id
name
slug {
name
}
}
}
}
}
}
onlyRoots: true returns only top-level families and superfamilies, not the subfamilies nested under them – see Managing your font catalog for how collections nest.
Typed results
fetchGraphql is generic, so until codegen runs nothing has told it what Index returns and the result is untyped. The codegen step reads each query file and writes a matching TypeScript type; passing that type to the helper types the whole result:
const data = await fetchGraphql<IndexQuery>("Index", IndexDoc);
Now data.viewer.fontCollections and everything under it is typed from the query, and an editor flags a field you didn’t ask for. Queries that take variables get a second generated type – FontQueryVariables – which goes in the helper’s second type slot.22The generated types land in operations-types.ts, and the template wires up the import path for you. Codegen is already wired into the template – npm run dev regenerates the types in watch mode as you edit queries. Set up GraphQL codegen explains how it’s configured and how to add it to a project of your own.
From data to pages
A typed query that returns collections and slugs is already enough to render a list of links into your catalog:
data.viewer?.fontCollections?.edges?.map((edge) => (
<a key={edge!.node!.id} href={`/fonts/${edge!.node!.slug?.name}`}>
{edge!.node!.name}
</a>
));
Each link points at a route that doesn’t exist yet – one detail page per collection, generated from the same data.33A plain <a> works anywhere; swap in your framework’s own link component for client-side navigation. That’s Build font pages.
?query=Index), which keeps requests legible in logs; the operation itself rides in the request body. ↩