Skip to content
Graffle is a work in progress. Learn more.

Static ​

Overview ​

The static document builder lets you build GraphQL documents statically without needing a client instance. It supports two syntaxes:

  • String - GraphQL syntax with type inference
  • Object - TypeScript objects with type-safe selection sets
ts
import { Graffle } from 'graffle'

// String syntax
const doc1 = Graffle.gql(`query { pokemons { name hp } }`)

// Object syntax
const doc2 = Graffle.gql({
  query: {
    getPokemons: {
      pokemons: { name: true, hp: true },
    },
  },
})

Once built, documents can be sent using the Instance API.

Root-Level Builders ​

For simpler queries and mutations, Graffle provides dedicated root-level builders that let you skip the operation wrapper:

Single Field Operations ​

Build queries and mutations for individual fields directly:

ts
import { mutation, query } from 'graffle'

// Query a single field
const getUserDoc = query.user({ id: true, name: true })
// Generates: { user { id name } }

// Mutate a single field
const createUserDoc = mutation.createUser({ id: true })
// Generates: mutation { createUser { id } }

Multi-Field Operations with $batch ​

Use $batch to select multiple root fields in a single operation:

ts
import { mutation, query } from 'graffle'

// Multiple queries
const batchQueryDoc = query.$batch({
  user: { id: true, name: true },
  posts: { id: true, title: true },
  comments: { id: true, text: true },
})
// Generates:
// {
//   user { id name }
//   posts { id title }
//   comments { id text }
// }

// Multiple mutations
const batchMutationDoc = mutation.$batch({
  createUser: { id: true },
  updatePost: { success: true },
  deleteComment: { success: true },
})
// Generates:
// mutation {
//   createUser { id }
//   updatePost { success }
//   deleteComment { success }
// }

When to Use Root-Level Builders

Use query and mutation for:

  • Quick, focused operations on single or multiple fields
  • Simple cases without complex nesting or variables
  • Schema-less workflows where you don't need generation

Use Graffle.gql() for:

  • Complex documents with variables and arguments
  • Multiple operations with different names
  • Full control over the document structure

Fields ​

Fields control which data appears in your GraphQL document using boolean values:

ts
const doc = Graffle.gql({
  query: {
    getPokemons: {
      pokemons: {
        name: true,
        hp: true,
      },
    },
  },
})

How it works:

  • true - Adds the field to the document
  • false - Removes the field from the document (useful with type spread patterns)
  • Nested objects - Select fields on related types
ts
const doc = Graffle.gql({
  query: {
    getPokemons: {
      pokemons: {
        name: true,
        trainer: {
          name: true,
          class: true,
        },
      },
    },
  },
})

Multiple Operations ​

ts
const doc = Graffle.gql({
  query: {
    getPokemon: {
      pokemonByName: {
        $: { name: $('name').required() },
        name: true,
        hp: true,
      },
    },
  },
  mutation: {
    addPokemon: {
      addPokemon: {
        $: {
          name: $.required(),
          type: $.required(),
        },
        name: true,
      },
    },
  },
})

Aliases ​

Aliases let you rename fields in your response or request the same field multiple times with different arguments.

Full Syntax ​

Works for all fields. Required when the field has arguments or non-scalar selection.

ts
const doc = Graffle.gql({
  query: {
    pokemons: [
      ['fire', {
        $: { filter: { type: 'FIRE' } },
        name: true,
      }],
      ['water', {
        $: { filter: { type: 'WATER' } },
        name: true,
      }],
    ],
  },
})

Short Array ​

Only for scalars/enums without required arguments. Equivalent to ['alias', true].

ts
const doc = Graffle.gql({
  query: {
    pokemon: {
      name: ['pokemonName'],
      hp: ['hitPoints'],
    },
  },
})

String Only ​

Only for scalars/enums without required arguments. Equivalent to ['alias', true]. Most concise.

ts
const doc = Graffle.gql({
  query: {
    pokemon: {
      id: 'pokemonId',
      name: 'pokemonName',
    },
  },
})

Directives ​

GraphQL directives like @skip and @include are written using special $ prefixed properties.

ts
const doc = Graffle.gql({
  query: {
    getTrainers: {
      trainers: {
        name: true,
        id: {
          $skip: true,
        },
        pokemon: {
          id: {
            $include: false,
          },
          name: true,
        },
      },
    },
  },
})

You can also apply directives to entire field groups using the special ___ key:

ts
const doc = Graffle.gql({
  query: {
    getPokemons: {
      ___: {
        $skip: true,
        pokemons: {
          name: true,
        },
      },
    },
  },
})

Enums ​

Enum values are passed as strings and automatically validated by TypeScript based on your schema.

ts
const doc = Graffle.gql({
  query: {
    getPokemons: {
      pokemons: {
        $: { filter: { type: 'FIRE' } },
        name: true,
        type: true,
      },
    },
  },
})

TypeScript will provide autocomplete for valid enum values and show errors for invalid ones.

Inline Fragments ​

Inline fragments are used to select fields on specific types in unions and interfaces. Use the ___on_TypeName syntax.

Unions ​

ts
const doc = Graffle.gql({
  query: {
    getBattles: {
      battles: {
        __typename: true,
        ___on_BattleRoyale: {
          date: true,
          combatants: {
            trainer: { name: true },
          },
        },
        ___on_BattleTrainer: {
          date: true,
          combatant1: {
            trainer: { name: true },
          },
        },
      },
    },
  },
})

Interfaces ​

Interface fragments work the same way as unions, using ___on_TypeName for each implementing type.

ts
const doc = Graffle.gql({
  query: {
    getBeings: {
      beings: {
        __typename: true,
        id: true,
        name: true,
        ___on_Patron: {
          money: true,
        },
        ___on_Trainer: {
          class: true,
        },
        ___on_Pokemon: {
          type: true,
        },
      },
    },
  },
})

Field Groups ​

Field groups allow you to apply directives to multiple fields at once using the special ___ key.

ts
const doc = Graffle.gql({
  query: {
    getPokemons: {
      ___: {
        $skip: true,
        pokemons: {
          name: true,
        },
      },
    },
  },
})

Arguments ​

ts
const doc = Graffle.gql({
  query: {
    getPokemon: {
      pokemonByName: {
        $: { name: 'Pikachu' },
        name: true,
        hp: true,
        attack: true,
      },
    },
  },
})

Variables ​

Defer Execution with Variables

When using variables with generated method calls (not Graffle.gql()), Graffle automatically switches to deferred execution, giving you a DocumentRunner object to inspect the document and execute it multiple times with different variables.

Basic Usage ​

ts
import { $ } from 'graffle'

const doc = Graffle.gql({
  query: {
    getPokemon: {
      pokemonByName: {
        $: { name: $('pokemonName').required() },
        name: true,
        type: true,
      },
    },
  },
})

Modifiers (Objects Only) ​

The object syntax provides additional control over variables:

ts
import { $ } from 'graffle'

// Required variable
$('name').required()

// Optional variable
$('name').optional()

// Variable with default value
$.default('Ash')

// Anonymous variable
$.required()

Schema-Less Mode Type Hints ​

When using the static builder without a generated schema, you can provide explicit type hints for variables using typed builder methods:

ts
import { $ } from 'graffle'

const doc = Graffle.gql({
  query: {
    getPokemon: {
      pokemonByName: {
        $: {
          name: $.String(), // → string
          level: $.Int(), // → number
          isShiny: $.Boolean(), // → boolean
          id: $.ID(), // → string
        },
        name: true,
      },
    },
  },
})

Available type hints:

  • $.String() - Maps to TypeScript string
  • $.Int() - Maps to TypeScript number
  • $.Float() - Maps to TypeScript number
  • $.Boolean() - Maps to TypeScript boolean
  • $.ID() - Maps to TypeScript string

Type inference rules:

  • With schema (generated client): Plain $ infers types from the schema
  • Without schema (static mode):
    • Plain $ → unknown (no type information)
    • $.String(), etc. → trusts the type hint you provide

Type hints can be combined with modifiers:

ts
$.String().required() // Required string variable
$.Int().default(10) // Optional number with default
$.Boolean().as('flag') // Boolean with custom name

Variable Hoisting

Graffle supports both manual hoisting (explicit $ markers) and automatic hoisting (extracting all arguments as variables). For a comprehensive guide on how hoisting works, configuration options, and choosing between approaches, see Variable Hoisting.

Picking a Syntax ​

Use Object when:

  • You want IDE autocomplete for all fields
  • You're building tools or libraries
  • You need programmatic document generation
  • You prefer TypeScript over GraphQL syntax

Use String when:

  • You're copying queries from GraphQL Playground
  • You have existing GraphQL documents
  • You prefer standard GraphQL syntax
  • You're migrating from another GraphQL client

Both syntaxes are equally powerful and type-safe - choose based on your preference and workflow.

Sending ​

Once built, pass documents to a client instance. See Sending for details.

Released under the MIT License.