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 }
}
}| Directive | Applies to | Note |
|---|---|---|
@include(if:) | Field, fragment spread | Variable must be non-null Boolean |
@skip(if:) | Field, fragment spread | The inverse of @include |
@deprecated | Schema fields and enums | Server-side, part of the schema not the query |
@defer | Fragment spread | Optional; 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.
Related
Queries, mutations and resolvers Schema design, evolution and tooling
Last refreshed 2026-09-18.