Queries: Fields, Arguments, Variables, And Fragments
Queries are the read side of GraphQL, and they carry most of the language's syntax. This lesson walks through that syntax piece by piece — fields, arguments, aliases, variables, fragments, and directives — using the music-catalog schema from the previous lesson.
Fields And Selection Sets
A query is a tree of field selections. Curly braces open a selection set: the list of fields you want from the thing above them.
query {
album(id: "al_301") {
title
releaseYear
artist {
name
}
}
}
Scalar fields (title, releaseYear) are leaves — they end the tree. Fields that return object types (artist) must have their own selection set; you can never ask for "the whole artist", only for named fields of it. That rule is what makes responses predictable.
Fields that return lists look identical in the query; the server just returns an array where each element has the selected shape.
The query keyword with no name can be omitted for quick reads ({ album(...) { ... } }), but in application code always name your operations:
query AlbumHeader {
album(id: "al_301") {
title
}
}
Named operations show up in server logs and error reports, and they are required once a document contains more than one operation.
Arguments
Any field — not just root fields — can accept arguments. This is a real difference from REST, where parameters effectively apply to the whole request.
query ArtistOverview {
artist(slug: "nova-tide") {
name
albums(first: 3, sort: RELEASE_DATE_DESC) {
title
}
topTracks(limit: 5) {
title
durationSeconds
}
}
}
Here artist takes a slug, while the nested albums field takes its own first and sort arguments. Arguments are typed by the schema — sort here is an enum, so a typo like RELEASE_DAT_DESC fails validation instead of silently returning unsorted data.
Aliases
Field names double as response keys, which creates a conflict if you want the same field twice with different arguments. Aliases rename the response key:
query CompareArtists {
headliner: artist(slug: "nova-tide") {
name
}
support: artist(slug: "paper-lanterns") {
name
}
}
The response contains headliner and support keys. Without the aliases this query would be invalid, since both fields would try to occupy the artist key.
Variables
Hard-coding values inside query strings invites string interpolation, and string-built queries are fragile and unsafe. Instead, declare typed variables in the operation signature and pass values separately as JSON:
query ArtistPage($slug: String!, $albumCount: Int = 10) {
artist(slug: $slug) {
name
albums(first: $albumCount) {
title
}
}
}
{ "slug": "nova-tide", "albumCount": 4 }
String! means the variable is required; $albumCount has a default so it may be omitted. The query document never changes between requests — only the variables JSON does — which lets servers and tools cache, log, and analyze queries as stable artifacts. Treat "never build query strings by concatenation" with the same seriousness as "never build SQL by concatenation."
Fragments
When several parts of a query (or several queries) need the same set of fields, extract a fragment:
fragment AlbumCard on Album {
id
title
releaseYear
coverUrl
}
query FestivalLineup {
headliner: artist(slug: "nova-tide") {
albums(first: 2) {
...AlbumCard
}
}
support: artist(slug: "paper-lanterns") {
albums(first: 2) {
...AlbumCard
}
}
}
A fragment is declared on a specific type and can only be spread where that type appears. Fragments may also use variables — albums(first: $albumCount) works inside a fragment body, as long as every operation that spreads the fragment declares $albumCount. In component-based frontends, fragments commonly live next to the component that renders those fields, so each component declares its own data needs.
Inline fragments handle fields that return interfaces or unions, where the concrete type varies per result:
query SearchEverything($term: String!) {
search(term: $term) {
... on Artist {
name
}
... on Album {
title
releaseYear
}
... on Track {
title
durationSeconds
}
}
}
Each ... on Type block applies only when the result is that concrete type. The __typename meta field can be added to any selection set to learn which type each object actually was — clients use it constantly when rendering mixed lists.
Directives: @include And @skip
Directives attach conditional behavior to selections. The two built into every server let a boolean variable switch parts of a query on or off:
query TrackList($albumId: ID!, $withCredits: Boolean!) {
album(id: $albumId) {
title
tracks {
title
credits @include(if: $withCredits) {
role
personName
}
}
}
}
@include(if:) fetches the field only when true; @skip(if:) is its mirror. This avoids maintaining two nearly identical query documents for "summary" and "detail" views. Schemas can also define custom directives, but treat those as server-specific extensions.
Multiple Operations Per Document
A single document may contain several named operations plus shared fragments; the request then specifies an operationName choosing which one to run. Only one operation executes per request — bundling documents is an organizational convenience, not batching.
What To Check
Before moving on, make sure you can:
- explain why every object-typed field requires a nested selection set
- use arguments on nested fields, not just root fields
- resolve a duplicate-field conflict with aliases
- rewrite a hard-coded query to use typed variables, and say why interpolation is dangerous
- choose between a named fragment and an inline fragment for a given situation
- make a field conditional with
@include/@skip
What You Should Be Able To Do
After this lesson, you should be able to write a non-trivial query against a schema you are given — with variables, aliases, fragments, and conditional fields — and predict the exact JSON shape that comes back, including for interface-typed fields where __typename and inline fragments come into play.