Writing queries: variables, fragments and directives

Named operations, variables with defaults, fragments on types and interfaces, aliases, and conditional inclusion with @include and @skip.

Variables instead of string building

query BooksByAuthor($authorId: ID!, $first: Int = 10, $includeReviews: Boolean!) {
  author(id: $authorId) {
    id
    name
    books(first: $first) {
      nodes {
        id
        title
        reviews @include(if: $includeReviews) {
          rating
          body
        }
      }
      pageInfo { hasNextPage endCursor }
    }
  }
}
{
  "authorId": "42",
  "first": 5,
  "includeReviews": true
}
  • The document is a constant string that can be persisted, cached and linted; only the variables change per call.
  • Variables are typed by the operation signature. A variable the operation declares but the schema cannot accept fails validation before execution.
  • A variable with a default is optional; one with a non-null type and no default is required, and omitting it is a validation error, not a null value.
  • Never interpolate user input into the query string - that is GraphQL injection and it also destroys client-side caching.
💡
Send the operation name. In a document with several operations the server cannot tell which one you mean, and your logs and traces become unreadable when everything is called "anonymous query".

Fragments, aliases and directives

fragment BookFields on Book {
  id
  title
  publishedAt
}

fragment AuthorFields on Person {
  name
  avatarUrl
}

query Homepage {
  featured: books(featured: true, first: 3) { nodes { ...BookFields } }
  recent: books(first: 3, orderBy: CREATED_AT_DESC) { nodes { ...BookFields } }
  editor: author(id: "1") {
    ...AuthorFields
    ... on Author { bookCount }
  }
}
DirectiveApplies toNote
@include(if:)Field, fragment spreadVariable must be non-null Boolean
@skip(if:)Field, fragment spreadThe inverse of @include
@deprecatedSchema fields and enumsServer-side, part of the schema not the query
@deferFragment spreadOptional; needs server and transport support
  • Aliases let one query fetch the same field with different arguments and keep distinct response keys - essential for a dashboard.
  • A fragment on an interface selects only interface fields; add an inline fragment with a type condition to reach a concrete type's fields.
  • Fragments are how client components request exactly what they render. Colocating a fragment with its component is the pattern that scales best.
  • Fragment cycles are a validation error. So is spreading a fragment on a type that cannot occur at that position.

Introspection in practice

query TypeInfo {
  __schema {
    queryType { name }
    types { name kind }
  }
}

query FieldDocs {
  __type(name: "Book") {
    name
    fields(includeDeprecated: false) {
      name
      type { name kind ofType { name } }
      args { name type { name } defaultValue }
    }
  }
}

{
  __typename
}
  • Introspection is what powers the explorer, autocomplete and code generation - it is a feature, not a leak.
  • In production, most teams disable it or restrict it to authenticated clients to reduce the surface for automated scanning.
  • Disabling introspection does not secure the API: field suggestions in error messages still leak names, so handle validation errors carefully too.

FAQ

Should our client write fragments or full queries?
Colocate fragments with the components that consume the fields, then compose them into page-level queries. That way adding a field to a component does not require editing every query that renders it.
Why does my variable get rejected as the wrong type?
The variable's declared type must be compatible with the argument type, including nullability. A nullable variable cannot be passed to a non-null argument - change the argument usage or declare the variable non-null.

Queries, mutations and resolvers Schema design, evolution and tooling

Last refreshed 2026-09-18.