Concept
The beginner framing: instead of a REST API's many fixed-shape endpoints (/users/1, /users/1/posts), GraphQL exposes one endpoint and lets the client specify exactly which fields it needs, in exactly the shape it wants them, in the request itself.
query {
user(id: 1) {
name
posts {
title
}
}
}{ "data": { "user": { "name": "Ada", "posts": [{ "title": "Hello" }] } } }The response shape mirrors the query shape exactly, no over-fetching (extra fields the client never asked for) and no under-fetching (needing a second request to get related data, the classic REST pain point that motivated GraphQL in the first place).
query {user(id: 1) {nameposts { title }}}
The query's top-level field, user(id: 1), resolves first. Its resolver typically hits a database or API to fetch the user.
Variables: parameterizing a query without string concatenation
query Greet($who: String!) {
greeting(name: $who)
}{ "who": "Ada" }Confirmed by executing this exact query against a real schema: $who is declared with a type (String!, required, non-null) and substituted from a separate variables object sent alongside the query, never by string-interpolating a value directly into the query text, which would be both error-prone and, for user-supplied values, a real injection risk.
Aliases: requesting the same field twice, differently
query {
a: greeting(name: "A")
b: greeting(name: "B")
}{ "data": { "a": "Hello, A!", "b": "Hello, B!" } }Confirmed by executing this: without aliases, requesting the same field twice with different arguments would collide in the response (both trying to occupy the same greeting key). Aliasing (a:, b:) lets each instance land under its own key.
Fragments: reusable field selections
query {
user { ...UserFields }
}
fragment UserFields on User {
id
name
email
}Confirmed by executing this: a fragment is a named, reusable set of fields for a given type, spread into a query with ...FragmentName. This matters most once a field selection is repeated across several queries/mutations in a real app, defining it once avoids the selections drifting out of sync with each other over time.
Mutations: the same syntax, a real execution-order guarantee
mutation {
first: incrementCounter
second: incrementCounter
third: incrementCounter
}{ "data": { "first": 1, "second": 2, "third": 3 } }Confirmed by executing this exact operation, repeatedly, with consistent results: multiple top-level mutation fields in a single operation execute strictly serially, the first completes before the second starts, and so on, which is why the counter increments predictably to 1, 2, 3 rather than in some unpredictable order. This is a genuine, spec-mandated difference from queries, where the GraphQL specification explicitly permits top-level fields to execute in any order (including in parallel), mutations are ordered specifically because they cause side effects, and unordered side effects would make application behavior unpredictable in a way unordered reads generally don't.
Try It
Predict the outcome before checking the solution.
query {
first: slowFetch(id: 1)
second: slowFetch(id: 2)
}Given that slowFetch is a query field (not a mutation) that takes noticeably different amounts of time to resolve depending on id, is first guaranteed to complete before second starts?
Solution
No, unlike mutations, the GraphQL specification does not guarantee any particular execution order for sibling query fields, and in practice, GraphQL execution engines commonly run independent top-level query fields concurrently precisely because there's no ordering requirement to respect. If second's underlying fetch happens to resolve faster than first's, there's nothing incorrect about that, the final response still correctly places each result under its own key (first, second), just potentially completed out of real-time order. This is the direct contrast to the mutation example above, where the serial guarantee is real and spec-mandated.
Implement It Yourself
Build a minimal query-variable substitution function, to internalize what "variables" actually do underneath the syntax:
function substituteVariables(queryTemplate, variables) {
// a GREATLY simplified stand-in for what a real GraphQL execution engine does:
// replace $varName references with their provided value
return queryTemplate.replace(/\$(\w+)/g, (match, varName) => {
if (!(varName in variables)) throw new Error(`Missing variable: $${varName}`);
return JSON.stringify(variables[varName]);
});
}
const template = `query { greeting(name: $who) }`;
console.log(substituteVariables
Real GraphQL execution engines do something considerably more sophisticated (type-checking each variable against its declared type, handling variables used in multiple places, nested input types), but this captures the essential idea: variables are substituted from a separate, structured source, never raw string concatenation.
Under the Hood
The query-resolution visualizer above previews the resolver execution model, each field, whether top-level or nested, is backed by its own resolver function, a mechanism covered in full depth (including the problem that arises when nested resolvers each trigger their own database call) in N+1 Problem & DataLoader. The overall schema shape, what types, fields, and arguments even exist to be queried, is covered in Schema Design.
Common Mistakes
1. String-interpolating values directly into a query instead of using variables
const query = `query { user(id: "${userInput}") { name } }`; // ❌ injection risk, error-proneBeyond the security risk with untrusted input, this also defeats query caching/parsing optimizations that rely on the query text staying constant across calls with different values, variables keep the query text identical while only the values change.
2. Assuming query fields execute in the order they're written
query {
slowField
fastField
}
# ❌ assuming slowField's result is guaranteed to be computed/available before fastField'sOnly mutations have a serial execution guarantee, confirmed above. Query field execution order (and completion order) is intentionally unspecified.
3. Forgetting to alias when requesting the same field with different arguments
query {
greeting(name: "A")
greeting(name: "B") # ❌ collides with the field above, same response key
}Without an alias, both invocations attempt to write to the same greeting key in the response, aliasing resolves the collision explicitly.
Best Practices
- Always use variables for any value that isn't a hardcoded literal, never string-concatenate user input or dynamic values directly into query text.
- Reach for fragments once a field selection is reused across more than one query/mutation, to keep them from drifting out of sync.
- Don't rely on query field execution order for anything correctness-sensitive, if strict ordering of side effects matters, that's what mutations are for.
- Alias deliberately whenever the same field is requested more than once in a single operation with different arguments.
Performance Tips
- Because query fields have no ordering requirement, a well-implemented GraphQL server can resolve independent top-level (and independent sibling) fields concurrently, this is a real, meaningful performance opportunity queries have that mutations, by design, don't.
- Fragments have no runtime performance cost or benefit of their own, they're a purely textual/organizational convenience, expanded during query parsing before execution begins.
