Skip to main content

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

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

OptionTypeDescription
namestringUnique query name. Becomes the derivation key.
documentTypedDocumentNode | stringGraphQL query – typed document or raw string.
variables(facts) => TVariables | nullDerive variables from facts. Return null to disable.
endpointstringGraphQL endpoint URL. Default: "/graphql".
headersRecord | (facts) => RecordRequest headers.
transform(result: TResult) => TDataTransform before caching.
extractData(response) => TResultCustom response envelope extraction.
onGraphQLError(errors) => voidHandle 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 onGraphQLError with 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
Previous
Convenience API

Stay in the loop. Sign up for our newsletter.

We care about your data. We'll never share your email.

Powered by Directive. This signup uses a Directive module with facts, derivations, constraints, and resolvers – zero useState, zero useEffect. Read how it works

Directive - Constraint-Driven Runtime for TypeScript | AI Guardrails & State Management