Skip to main content

Class 4: Queries In Depth

Learn how to fetch data from a GraphQL server, and master it with aliases, fragments, variables, and directives.

Duration: ~15 minutes | Difficulty: Beginner | Prerequisites: Class 3


What is a Query?

A query is a read-only operation that fetches data from a GraphQL server. It's the most common operation type and the foundation of GraphQL's client-driven data fetching.

In GraphQL, you need to tell the server which fields you want:

query Movies {
movies {
title
releaseYear
}
}

And the server will return those fields in the same structure as the request:

{
"data": {
"movies": [
{ "title": "The Matrix", "releaseYear": 1999 },
{ "title": "Inception", "releaseYear": 2010 }
]
}
}

The response mirrors the query structure exactly. This predictability is one of GraphQL's core strengths.

Movies is the operation name of this query. GraphQL also accepts operations without a name, called anonymous operations, and Named Operations below explains why an application should name every one.


Named Operations

Every operation has an operation type, which is query, mutation or subscription, and it can have an operation name, the identifier written after the type. In query Movies { ... } the type is query and the name is Movies. Developers, tools and server logs use that name to refer to the operation.

query Movies {
movies {
title
}
}

Anonymous Operations and Query Shorthand

An operation without a name is an anonymous operation. The GraphQL specification allows it, so this document is valid:

query {
movies {
title
}
}

For simple reads, GraphQL also lets you leave out the query keyword. The specification calls this form query shorthand. It is allowed when the document contains only one operation, and that operation is a query without variables or directives:

{
movies {
title
}
}

Both documents are anonymous operations, and the server runs them exactly as it runs Movies. You will often meet the shorthand in the specification and in short examples, because it is the shortest way to write a query.

Anonymous operations have one hard limit. The Lone Anonymous Operation validation rule allows an anonymous operation only when it is the single operation in its document. This document therefore fails validation:

query {
movies { title }
}

query Movie {
movie(id: "1") { title }
}

graphql-js rejects it with the message "This anonymous operation must be the only defined operation.", and graphql-java reports "Anonymous operation with other operations". Once both operations have names, the document is valid, and Multiple Operations in One Document shows how a client picks the one to run.

Why Name Every Operation

The specification requires names only in documents with several operations. The official guidance goes further and recommends a name on every operation:

  • graphql.org says on its Queries page that the name "is required when sending multiple operations in one document, but even if you're only sending one operation it's encouraged because operation names are helpful for debugging and server-side logging."
  • Apollo puts "Name all operations" first in its operation best practices, and marks an anonymous query { ... } as "Not recommended". Apollo gives four reasons: teammates can see what each operation is for, a document that combines several operations stays valid, debugging output on the client and the server gets clearer, and Apollo GraphOS Studio needs names for its operation-level metrics.
  • GraphQL-ESLint enforces the advice with its no-anonymous-operations rule, which is part of its recommended configuration. The rule's description adds that "most GraphQL client libraries are using the operation name for caching purposes."

The name matters most when something fails. A log line or a trace that says Movies points at one place in the client code, while an anonymous operation has to be recognized by its fields. Spring for GraphQL, for example, records the name of every request as graphql.operation.name, and our Spring GraphQL course shows how that name reaches your traces.

Choosing an Operation Name

The specification leaves the format of a name open, and graphql.org asks you to "pick a meaningful name". This course follows the examples on graphql.org's Queries, Mutations and Subscriptions pages, all in PascalCase:

  • A query name describes what the query fetches, like HeroNameAndFriends.
  • A mutation name starts with the action, like CreateReviewForEpisode.
  • A subscription name describes the event, like NewReviewCreated.

Apollo's documentation prefixes query names with Get, as in GetBooks; this site follows graphql.org's style. graphql.org also asks for a "unique operation name", so operations that fetch different data get different names.

Multiple Operations in One Document

A single GraphQL document can hold multiple named operations. The client then picks which one to execute by sending an operationName field alongside the query in the HTTP request body.

query Movie($id: ID!) {
movie(id: $id) {
title
}
}

query Person($id: ID!) {
person(id: $id) {
name
}
}

mutation DeleteMovie($id: ID!) {
deleteMovie(id: $id) {
success
}
}

Sent as:

{
"query": "query Movie(...) { ... } query Person(...) { ... } mutation DeleteMovie(...) { ... }",
"operationName": "Person",
"variables": { "id": "42" }
}

Only Person runs. The rest of the document is ignored for this request.

When is this useful?

  • Code generation: tools like Relay's and Apollo's compilers extract every gql-tagged query from your source, bundle them into one document, and the runtime sends operationName to pick one.
  • Persisted documents: the hash is computed over the entire document, and operationName selects which operation within the document to execute.
  • Fragment sharing: a document with multiple operations can share named fragment definitions between them without duplication.

If a document contains more than one operation, operationName is required, because the server cannot tell which one to run otherwise. For a single-operation document operationName is optional, and the operation should still have a name, for the reasons above.


Arguments

Fields can accept arguments to filter, paginate, or customise results:

query MovieById {
movie(id: "1") {
title
releaseYear
}
}

query MoviesReleasedIn2024 {
movies(releaseYear: 2024, limit: 10) {
title
}
}

Arguments can be:

  • Required: movie(id: ID!) - must be provided
  • Optional: movies(genre: Genre) - can be omitted
  • With defaults: movies(limit: Int = 20) - uses default limit of 20, if omitted

These examples write the values straight into the query, which keeps the argument syntax easy to read. In an application, a value like a movie ID comes from the user or the page, and Variables shows how to pass it.


Variables

The examples so far write every argument value directly into the query. graphql.org points out that "in most applications, the arguments to fields will be dynamic", such as a movie the user clicked, text typed into a search box, or a set of filters. Building a new query string for every value is fragile, so GraphQL provides variables, which carry the values separately from the query.

Using a variable takes three steps, which graphql.org's Variables section lists:

  1. Replace the static value in the query with $variableName.
  2. Declare $variableName as one of the variables the operation accepts.
  3. Pass the value in a separate variables dictionary, which is usually JSON.
query Movie($movieId: ID!) {
movie(id: $movieId) {
title
releaseYear
rating
}
}

Variables are passed separately from the query:

{
"movieId": "42"
}

Declaring Variables on the Operation

A variable belongs to the operation that declares it. The declarations sit in parentheses right after the operation name, like ($movieId: ID!) above. The specification says "Variables must be defined at the top of an operation and are in scope throughout the execution of that operation." Each declaration has a name and a type, and the type has to fit the argument that receives the value: movie(id: ID!) takes an ID!.

Validation checks the declarations in both directions. The All Variable Uses Defined rule rejects a variable that the operation uses without declaring it:

query Movie {
movie(id: $movieId) {
title
}
}

graphql-js reports Variable "$movieId" is not defined by operation "Movie"., and graphql-java reports Undefined variable 'movieId'. The All Variables Used rule rejects the opposite case, a declared variable that nothing uses, which graphql-js reports as Variable "$movieId" is never used in operation "Movie".

graphql.org adds that "You must specify an operation type and name in a GraphQL document to use variables", and the query shorthand cannot declare variables at all. GraphQL-ESLint catches these mistakes while you edit, through the no-undefined-variables, no-unused-variables and variables-in-allowed-position rules in its recommended set.

Variable Syntax

Variable Anatomy
query Movie($movieId: ID!, $includeReviews: Boolean = false) { ... }
PartDescription
$movieIdVariable name
ID!Type (required)
$includeReviewsVariable name
BooleanType (optional)
= falseDefault value

Usage in query: movie(id: $movieId) { ... } - reference the variable with $

Why Variables Matter

  • Reuse. One operation serves every value. Apollo's operation best practices note that a query with a variable can fetch an object with any ID, "making it much more reusable."
  • No string building. The client sends the same document every time and never splices user input into it. graphql.org's rule is that "we should never be doing string interpolation to construct queries from user-supplied values."
  • Caching. Apollo explains under Disadvantages of hardcoded GraphQL arguments that queries differing only in hardcoded values are "considered entirely different operations by your GraphQL server's cache", so the server parses and validates each one again.
  • Privacy. A sensitive value, such as an access token or personal data, can end up in the server's cache: when it is written into the query string, Apollo notes, "it's cached with the rest of that query string."
  • Type safety. The server checks each variable value against its declared type before execution starts, and rejects a value that does not fit.

When a Literal Is Fine

graphql.org's advice is about dynamic arguments in applications. Documentation writes literal values in examples all the time, because they are easier to read, and the specification's first example is { user(id: 4) { name } }. This course follows the same split. Examples that explain a concept, walk through a response or an error, or run in a test write their values directly into the query. Client application code, where a real value comes from the user or the page, uses variables. A value that defines what the query is for, like sortBy: RATING in the top-rated list under Fragments, can stay literal anywhere.


Aliases

When you need to query the same field multiple times with different arguments, use aliases:

query CompareTwoMovies {
originalMatrix: movie(id: "1") {
title
releaseYear
}
theWorstSequel: movie(id: "2") {
title
releaseYear
}
}

Response:

{
"data": {
"originalMatrix": {
"title": "The Matrix",
"releaseYear": 1999
},
"theWorstSequel": {
"title": "The Matrix Resurrections",
"releaseYear": 2021
}
}
}

Without aliases, this query would fail because movie appears twice at the same level. I know the above is not a very useful example, but I will never stop complaining about the Matrix sequels. A realistic scenario where this might be used is, for example, when you need to display the same image twice on a single page, but at different sizes and resolutions.

Alias Use Cases

  • Comparing items side-by-side, for example, comparing the features of two smartphones
  • Fetching the same data with different filters, like the image we talked about above
  • Renaming fields for client convenience

Fragments

Fragments are reusable units of fields. They let you construct sets of fields, and then include them in queries where needed, without duplicating the same fields everywhere. Here’s an example of how you could use fragments:

Basic Fragment

fragment MovieDetails on Movie {
id
title
releaseYear
rating
genres
}

query TopRatedAndNewestMovies {
topRated: movies(sortBy: RATING, limit: 5) {
...MovieDetails
}
newest: movies(sortBy: RELEASE_DATE, limit: 5) {
...MovieDetails
}
}

The ...MovieDetails syntax spreads the fragment's fields into the selection.

Fragment Syntax

Fragment Anatomy
fragment MovieDetails on Movie { ... }
PartDescription
fragmentKeyword
MovieDetailsFragment name
on MovieType the fragment applies to

Usage: ...MovieDetails - the spread operator includes all fragment fields

Inline Fragments

For one-off use or when working with interfaces/unions:

query SearchMoviesAndTvShows {
search(query: "matrix") {
... on Movie {
title
duration
}
... on TvShow {
title
seasons
}
}
}

Inline fragments are essential when querying union types or interfaces where different types have different fields.


Directives

Directives modify query execution. They're prefixed with @.

Built-in Directives

The GraphQL spec defines a small set of built-in directives. The two you will use most often inside queries are @include and @skip (these are the executable directives that affect how a document runs). The spec also ships type-system directives that show up in schemas instead of in queries. @deprecated marks fields, enum values, arguments, and input fields as deprecated, and @specifiedBy points custom scalars at the URL describing their format. @oneOf, added in the 2025 spec release, marks an input object as a discriminated union where exactly one field must be set. We focus on @include and @skip below because those are the ones you write in a client query.

@include

Include a field only if the condition is true:

query MovieWithOptionalReviews($movieId: ID!, $withReviews: Boolean!) {
movie(id: $movieId) {
title
releaseYear
reviews @include(if: $withReviews) {
rating
comment
}
}
}

@skip

Skip a field if the condition is true (opposite of @include):

query MovieWithOptionalReviews($movieId: ID!, $skipReviews: Boolean!) {
movie(id: $movieId) {
title
reviews @skip(if: $skipReviews) {
rating
}
}
}

Directive Use Cases

  • Conditional field fetching based on user preferences
  • Feature flags in the client
  • Reducing payload size when data isn't needed

Custom Directives

GraphQL servers can define custom directives for:

  • Authorization (@auth, @hasRole)
  • Caching hints (@cacheControl)
  • Cost and rate limiting (@cost, @rateLimit)
  • Formatting (@uppercase, @formatDate)

These are implementation-specific and defined in the schema. @deprecated, @specifiedBy and @oneOf are not in this group: the specification defines them, so a server that needs them provides them under those exact names instead of inventing its own. Not every schema exposes all three, because the spec asks for @specifiedBy and @oneOf only when the schema actually uses custom scalars or OneOf input objects.


Nested Queries

One of GraphQL's superpowers is fetching related data in a single request:

query MovieWithDetails {
movie(id: "1") {
title
director {
name
nationality
movies {
title
releaseYear
}
}
actors {
name
awards {
name
category
}
}
reviews {
rating
user {
displayName
}
}
}
}

This single query traverses:

  • Movie → Director → Director's other movies
  • Movie → Actors → Actor's awards
  • Movie → Reviews → Review authors

In REST, this might require 5+ separate API calls.


Query Execution

Understanding how queries execute helps write efficient queries:


Best Practices

1. Always Name Your Operations

An anonymous operation, one without a name, is valid GraphQL, but it makes logs, traces and metrics harder to read. Named Operations covers the rule and the sources behind it.

# ❌ Anonymous
query {
movies { title }
}

# ✅ Named
query AllMovies {
movies { title }
}

2. Use Variables for Dynamic Values

A value that changes from request to request belongs in a variable declared on the operation, as Variables explains.

# ❌ Hardcoded
query Movie {
movie(id: "123") { title }
}

# ✅ Variable
query Movie($id: ID!) {
movie(id: $id) { title }
}

3. Extract Repeated Fields into Fragments

# ❌ Duplicated
query TwoMovies {
movie1: movie(id: "1") {
id
title
releaseYear
rating
}
movie2: movie(id: "2") {
id
title
releaseYear
rating
}
}

# ✅ Fragment
fragment MovieFields on Movie { id title releaseYear rating }

query TwoMovies {
movie1: movie(id: "1") { ...MovieFields }
movie2: movie(id: "2") { ...MovieFields }
}

4. Request Only What You Need

# ❌ Over-fetching
query MovieDetailView {
movie(id: "1") {
id
title
releaseYear
rating
duration
genres
director {
name
bio
birthYear
nationality
}
actors {
name
bio
birthYear
nationality
}
reviews {
rating
title
comment
createdAt
user {
name
email
}
}
awards {
name
category
year
status
}
}
}

# ✅ Just what's needed for this view, you can change this without changing the API
query MovieDetailView {
movie(id: "1") {
title
releaseYear
rating
}
}

Summary

ConceptPurposeSyntax
Named OperationIdentify queries for debugging/loggingquery Movies { ... }
Anonymous OperationValid, but discouraged in applications{ movies { title } }
ArgumentsFilter/customize field resultsmovie(id: "1")
VariablesDynamic, type-safe valuesquery Movie($id: ID!) { movie(id: $id) }
AliasesRename fields / query same field twicefirst: movie(id: "1")
FragmentsReusable field selectionsfragment X on Type { ... }
DirectivesModify execution conditionally@include(if: $bool)

What's Next?

In the next class, we'll explore Class 5: Mutations - how to create, update, and delete data with GraphQL.