Data Fetching
•8 min read
GraphQL
First-class GraphQL support with end-to-end type safety. Works with graphql-codegen's TypedDocumentNode or raw query strings.
Install
No extra dependencies needed – GraphQL support is built into @directive-run/query.
npm install @directive-run/query @directive-run/core
For type safety, add graphql-codegen to your project:
npm install -D @graphql-codegen/cli @graphql-codegen/typed-document-node @graphql-codegen/typescript @graphql-codegen/typescript-operations
Quick Start
With TypedDocumentNode (recommended)
import { createQuerySystem } from "@directive-run/query";
import { createGraphQLQuery } from "@directive-run/query";
import { GetUserDocument } from "./generated";
const user = createGraphQLQuery({
name: "user",
document: GetUserDocument,
variables: (facts) => {
const userId = facts.userId as string;
if (!userId) {
return null;
}
return { id: userId };
},
});
The document parameter carries both the query string and the TypeScript types. Variables are type-checked, and the response is fully typed.
With Raw Query String
import { createGraphQLQuery } from "@directive-run/query";
const user = createGraphQLQuery({
name: "user",
document: `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`,
variables: (facts) => {
const userId = facts.userId as string;
if (!userId) {
return null;
}
return { id: userId };
},
});
createGraphQLQuery
Full options:
createGraphQLQuery({
// Required
name: "user",
document: GetUserDocument,
variables: (facts) => facts.userId ? { id: facts.userId } : null,
// GraphQL-specific
endpoint: "/api/graphql",
headers: { Authorization: "Bearer token" },
extractData: (response) => response.data,
onGraphQLError: (errors) => console.error("GQL errors:", errors),
// Transform the response before caching
transform: (result) => ({
displayName: result.user.name.toUpperCase(),
}),
// All standard query options work
tags: ["users"],
refetchAfter: 30_000,
keepPreviousData: true,
retry: 3,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
onSuccess: (data) => console.log("Fetched:", data),
onError: (error) => console.error("Failed:", error),
});
Options
| Option | Type | Description |
|---|---|---|
name | string | Unique query name. Becomes the derivation key. |
document | TypedDocumentNode | string | GraphQL query – typed document or raw string. |
variables | (facts) => TVariables | null | Derive variables from facts. Return null to disable. |
endpoint | string | GraphQL endpoint URL. Default: "/graphql". |
headers | Record | (facts) => Record | Request headers. |
transform | (result: TResult) => TData | Transform before caching. |
extractData | (response) => TResult | Custom response envelope extraction. |
onGraphQLError | (errors) => void | Handle GraphQL errors array. |
Plus all standard query options: tags, refetchAfter, retry, keepPreviousData, enabled, dependsOn, structuralSharing, refetchOnWindowFocus, refetchOnReconnect, refetchInterval, suspense, throwOnError, onSuccess, onError, onSettled.
createGraphQLClient
Share endpoint, headers, and auth across multiple queries.
import { createGraphQLClient } from "@directive-run/query";
const gql = createGraphQLClient({
endpoint: "/api/graphql",
headers: () => ({
Authorization: `Bearer ${getToken()}`,
}),
});
const user = gql.query({
name: "user",
document: GetUserDocument,
variables: (facts) => {
const userId = facts.userId as string;
if (!userId) {
return null;
}
return { id: userId };
},
});
const posts = gql.query({
name: "posts",
document: GetPostsDocument,
variables: () => ({ limit: 10 }),
tags: ["posts"],
});
Per-query headers merge with client headers (query headers take precedence).
With createQuerySystem
Use GraphQL queries inline in the simple path:
import {
createQuerySystem,
createGraphQLQuery,
createGraphQLClient,
} from "@directive-run/query";
const gql = createGraphQLClient({
endpoint: "/api/graphql",
headers: () => ({
Authorization: `Bearer ${getToken()}`,
}),
});
const app = createQuerySystem({
facts: { userId: "", postId: "" },
// Mix GraphQL and REST queries freely
queries: {
// REST query
notifications: {
key: () => ({ all: true }),
fetcher: async (p, signal) => {
const res = await fetch("/api/notifications", { signal });
return res.json();
},
},
},
});
// GraphQL queries via the advanced path
const userQuery = gql.query({
name: "user",
document: GetUserDocument,
variables: (facts) => {
const userId = facts.userId as string;
if (!userId) {
return null;
}
return { id: userId };
},
tags: ["users"],
});
With Multi-Module Systems
Compose GraphQL data modules with other modules:
import { createSystem, t } from "@directive-run/core";
import {
createQueryModule,
createGraphQLQuery,
createGraphQLClient,
createMutation,
} from "@directive-run/query";
const gql = createGraphQLClient({
endpoint: "/api/graphql",
headers: () => ({
Authorization: `Bearer ${getToken()}`,
}),
});
// Data module – all GraphQL queries and mutations
const dataModule = createQueryModule("data", [
gql.query({
name: "user",
document: GetUserDocument,
variables: (f) => f.userId ? { id: f.userId } : null,
tags: ["users"],
}),
gql.query({
name: "posts",
document: GetPostsDocument,
variables: (f) => ({
authorId: f.userId,
limit: 20,
}),
tags: ["posts"],
}),
createMutation({
name: "updateUser",
mutator: async (vars, signal) => {
const res = await fetch("/api/graphql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: `mutation UpdateUser($id: ID!, $name: String!) {
updateUser(id: $id, name: $name) { id name }
}`,
variables: vars,
}),
signal,
});
return (await res.json()).data.updateUser;
},
invalidateTags: ["users"],
}),
], {
schema: { facts: { userId: t.string() } },
init: (f) => { f.userId = ""; },
});
// Compose with auth and UI modules
const system = createSystem({
modules: {
data: dataModule,
auth: authModule,
ui: uiModule,
},
});
system.start();
// Namespaced access
system.facts.data.userId = "42";
await system.settle();
system.read("data.user"); // GraphQL user data
system.read("data.posts"); // GraphQL posts data
With AI Single-Agent
Use GraphQL to fetch context for AI agents:
import {
createQuerySystem,
createGraphQLQuery,
createGraphQLClient,
} from "@directive-run/query";
const gql = createGraphQLClient({ endpoint: "/api/graphql" });
const app = createQuerySystem({
facts: { topic: "", context: "" },
subscriptions: {
// AI agent streams responses
agent: {
key: (f) => {
if (!f.topic || !f.context) {
return null;
}
return { topic: f.topic, context: f.context };
},
subscribe: (params, { onData, onError, signal }) => {
let response = "";
fetch("/api/ai/stream", {
method: "POST",
body: JSON.stringify({
prompt: `Given this context: ${params.context}\n\nAnalyze: ${params.topic}`,
}),
signal,
}).then(async (res) => {
const reader = res.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
response += decoder.decode(value);
onData(response);
}
}).catch((err) => {
if (!signal.aborted) {
onError(err);
}
});
},
},
},
});
// GraphQL fetches context, then AI agent uses it
// 1. Fetch related documents via GraphQL
const docs = gql.query({
name: "relatedDocs",
document: SearchDocumentsDocument,
variables: (f) => f.topic ? { query: f.topic, limit: 5 } : null,
});
// 2. When docs load, set context fact – which triggers the AI agent subscription
// The constraint engine handles the dependency chain automatically
With AI Multi-Agent Orchestrator
GraphQL modules integrate seamlessly with multi-agent systems:
import { createSystem, t } from "@directive-run/core";
import {
createQueryModule,
createGraphQLClient,
} from "@directive-run/query";
import { createMultiAgentOrchestrator } from "@directive-run/ai";
const gql = createGraphQLClient({ endpoint: "/api/graphql" });
// Data layer – fetches all external data via GraphQL
const dataModule = createQueryModule("data", [
gql.query({
name: "customerProfile",
document: GetCustomerDocument,
variables: (f) => f.customerId ? { id: f.customerId } : null,
}),
gql.query({
name: "orderHistory",
document: GetOrdersDocument,
variables: (f) => f.customerId ? { customerId: f.customerId, limit: 50 } : null,
}),
gql.query({
name: "productCatalog",
document: GetProductsDocument,
variables: () => ({ active: true }),
}),
], {
schema: { facts: { customerId: t.string() } },
init: (f) => { f.customerId = ""; },
});
// AI orchestrator – agents can read GraphQL data via cross-module deps
const orchestrator = createMultiAgentOrchestrator({
agents: {
researcher: {
// Reads data.customerProfile and data.orderHistory
// to build context for recommendations
},
recommender: {
// Reads data.productCatalog + researcher output
// to generate personalized suggestions
},
writer: {
// Takes recommender output and writes customer-facing copy
},
},
modules: {
data: dataModule,
},
});
// Set the customer – all GraphQL queries fire,
// then agents process the results in sequence
orchestrator.facts.data.customerId = "cust_42";
Error Handling
GraphQL Errors
GraphQL can return both data and errors. By default, createGraphQLQuery handles this:
- No data + errors – throws the first error message
- Data + errors (partial) – returns data, calls
onGraphQLErrorwith the errors array - HTTP error (non-200) – throws with status code
const user = createGraphQLQuery({
name: "user",
document: GetUserDocument,
variables: () => ({ id: "1" }),
onGraphQLError: (errors) => {
// Called for both full and partial errors
for (const err of errors) {
console.warn(`GraphQL: ${err.message}`, err.path);
}
},
});
Custom Error Extraction
Override how data is extracted from the response envelope:
const user = createGraphQLQuery({
name: "user",
document: GetUserDocument,
variables: () => ({ id: "1" }),
extractData: (response) => {
if (response.errors?.length) {
throw new Error(response.errors.map((e) => e.message).join(", "));
}
return response.data!;
},
});
Type Safety
With graphql-codegen
The full type chain flows automatically:
GraphQL Schema
→ graphql-codegen generates TypedDocumentNode<TResult, TVariables>
→ createGraphQLQuery infers TResult and TVariables from the document
→ variables() is type-checked against TVariables
→ fetcher sends typed variables to the endpoint
→ response is typed as TResult
→ transform maps TResult → TData (optional)
→ ResourceState<TData> in the derivation
Utility Types
Extract types from a TypedDocumentNode:
import type { ResultOf, VariablesOf } from "@directive-run/query";
import { GetUserDocument } from "./generated";
type UserResult = ResultOf<typeof GetUserDocument>;
// { user: { id: string; name: string; email: string } }
type UserVars = VariablesOf<typeof GetUserDocument>;
// { id: string }
Debugging
explainQuery works with GraphQL queries the same as REST:
app.explain("user");
// Query "user"
// Status: success (fresh)
// Cache key: {"id":"42"}
// Data age: 12s
// Last fetch causal chain:
// Fact changed: userId "" -> "42"
// Constraint: _q_user_fetch (priority 50)
// Resolved in: 234ms

