Variables make operations reusable, cacheable and safe from injection.
Any field can declare arguments, e.g. book(id: ID!) or books(genre: Genre, first: Int = 10). Arguments can have default values in the schema.
Hard-coding values into the query string works for exploration, but real clients use variables: the operation declares typed placeholders, and values travel separately as JSON.
query Book($id: ID!, $withReviews: Boolean = false) {
book(id: $id) { title }
}The HTTP body then carries {"query": "...", "variables": {"id": "b1"}}.
Why this matters:
- The query text stays constant, so it can be cached, persisted and logged by name.
- Values are validated against declared types; you never build query strings with string concatenation, which avoids injection bugs.
- Variable types must be compatible with where they are used:
$id: ID(nullable) cannot be passed to anID!argument unless it has a default.
Complex arguments use input types, declared with input instead of type. Input types may only contain scalars, enums and other input types, never output object types.
Directives @include(if: $flag) and @skip(if: $flag) let variables toggle parts of a selection.
Example
import { buildSchema, graphql } from "graphql";
const schema = buildSchema(`
type Query { greet(name: String!, excited: Boolean = false): String! }
`);
const rootValue = {
greet: ({ name, excited }) => `Hello, ${name}${excited ? "!" : "."}`,
};
const source = `query Greet($who: String!, $loud: Boolean) {
greet(name: $who, excited: $loud)
}`;
const res = await graphql({
schema, rootValue, source,
variableValues: { who: "Ann", loud: true },
});
console.log(JSON.stringify(res.data)); // {"greet":"Hello, Ann!"}Remember
- Declare
$varswith types in the operation, send values as JSON - Never build query strings by concatenating user input
- Complex arguments use
inputtypes @include/@skiptoggle fields from variables
Watch out for
- Passing a nullable variable to a non-null argument without a default; validation rejects it.