Validation & Execution
Understand how GraphQL validates and executes queries step by step.
The Request Lifecycle
Every GraphQL request goes through a defined pipeline:
┌─────────────────────────────────────────────────────────────────────┐
│ GRAPHQL REQUEST PIPELINE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. RECEIVE │
│ HTTP request arrives with query string │
│ │ │
│ ▼ │
│ 2. PARSE │
│ Convert query string to AST (Abstract Syntax Tree) │
│ │ │
│ ▼ │
│ 3. VALIDATE │
│ Check AST against schema rules │
│ │ │
│ ▼ │
│ 4. EXECUTE │
│ Resolve each field, fetch data │
│ │ │
│ ▼ │
│ 5. RESPOND │
│ Return JSON response to client │
│ │
└─────────────────────────────────────────────────────────────────────┘
Let's explore each step in detail.
Step 1: Parsing
The parser converts a query string into an Abstract Syntax Tree (AST).
Input (Query String)
query GetMovie($id: ID!) {
movie(id: $id) {
title
releaseYear
}
}
Output (AST Representation)
Document
└── OperationDefinition (query "GetMovie")
├── VariableDefinitions
│ └── VariableDefinition ($id: ID!)
└── SelectionSet
└── Field (movie)
├── Arguments
│ └── Argument (id: $id)
└── SelectionSet
├── Field (title)
└── Field (releaseYear)
Syntax Errors
If the query has invalid syntax, parsing fails:
query {
movie(id: "1" { # Missing )
title
}
}
Error:
{
"errors": [{
"message": "Syntax Error: Expected ')', found '{'",
"locations": [{ "line": 2, "column": 17 }]
}]
}
Parsing errors stop the pipeline - no validation or execution occurs.
Step 2: Validation
The validator checks the AST against schema rules. GraphQL defines many validation rules.
Field Validation
Rule: Every field must exist on its parent type.
query {
movie(id: "1") {
title
nonExistentField # ❌ Invalid
}
}
Argument Validation
Rule: Required arguments must be provided. Argument types must match.
# Schema: movie(id: ID!): Movie
query {
movie { # ❌ Missing required argument 'id'
title
}
}
query {
movie(id: 123) { # ❌ ID expected, got Int
title
}
}
Fragment Validation
Rule: Fragments can only be used on compatible types.
fragment MovieFields on Movie {
title
}
query {
actor(id: "1") {
...MovieFields # ❌ Can't use Movie fragment on Actor
}
}
Variable Validation
Rule: Variable types must be compatible with their usage.
query GetMovie($id: String!) { # Variable is String!
movie(id: $id) { # ❌ Argument expects ID!
title
}
}
Directive Validation
Rule: Directives must be used in valid locations.
query @include(if: true) { # ❌ @include is for fields, not operations
movie(id: "1") {
title
}
}
Validation Rules Reference
GraphQL spec defines many validation rules:
| Category | Examples |
|---|---|
| Operations | Named operations must be unique |
| Fields | Fields must exist, leaf fields can't have selections |
| Arguments | Required args provided, types match, no duplicates |
| Fragments | Must be used, no cycles, type conditions valid |
| Values | Correct types, enums valid, required fields present |
| Directives | Valid locations, required args provided |
| Variables | Defined before use, types compatible, used at least once |
Step 3: Execution
Once validated, the query is executed by resolving each field.
Execution Order
┌─────────────────────────────────────────────────────────────────────┐
│ EXECUTION ORDER │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ query { │
│ movie(id: "1") { # 1. Resolve Query.movie │
│ title # 2. Resolve Movie.title │
│ director { # 3. Resolve Movie.director │
│ name # 4. Resolve Director.name │
│ } │
│ actors { # 3. Resolve Movie.actors (parallel) │
│ name # 5. Resolve Actor.name (for each) │
│ } │
│ } │
│ } │
│ │
│ Execution rules: │
│ - Root fields execute first │
│ - Child fields wait for parent to resolve │
│ - Sibling fields may execute in parallel │
│ - List items resolve in parallel │
│ │
└─────────────────────────────────────────────────────────────────────┘
Resolvers
Each field has a resolver function that returns its value:
Field: movie(id: "1")
Resolver: movieResolver(parent, args, context)
- parent: null (root query)
- args: { id: "1" }
- context: { user, dataSources }
- returns: Movie object
Field: Movie.title
Resolver: titleResolver(parent, args, context)
- parent: Movie { id: "1", title: "Inception", ... }
- returns: "Inception" (often just parent.title)
Default Resolvers
For simple fields, GraphQL uses default resolvers that:
- Look for a property with the same name on the parent object
- Call it as a function if it is one
- Return the value
// These are equivalent:
Movie.title → movie.title
Movie.releaseYear → movie.releaseYear
Custom resolvers are only needed when:
- Field name differs from property name
- Data requires transformation
- Data must be fetched from another source
Parallel vs Sequential Execution
Queries: Parallel
Root query fields may execute in parallel:
query {
movie(id: "1") { title } # Can run in parallel
actor(id: "2") { name } # Can run in parallel
topRatedMovies { title } # Can run in parallel
}
Mutations: Sequential
Root mutation fields execute sequentially (in order):
mutation {
first: createMovie(input: {...}) { id } # Executes first
second: createReview(input: {...}) { id } # Waits for first
third: updateMovie(input: {...}) { id } # Waits for second
}
This guarantees mutations execute in the order specified.
Type Resolution
For interfaces and unions, the executor must determine the concrete type:
interface Content {
id: ID!
title: String!
}
type Movie implements Content {
id: ID!
title: String!
duration: Int!
}
type TvShow implements Content {
id: ID!
title: String!
seasons: Int!
}
Query:
query {
search(query: "matrix") {
__typename # Returns "Movie" or "TvShow"
id
title
... on Movie { duration }
... on TvShow { seasons }
}
}
The server must implement a resolveType function to determine which concrete type each result is.
Execution Context
Resolvers receive a context object containing:
┌─────────────────────────────────────────────────────────────────────┐
│ RESOLVER CONTEXT │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ context = { │
│ // Authentication │
│ user: { id: "123", role: "admin" }, │
│ │
│ // Data access │
│ dataSources: { │
│ movieDB: MovieDataSource, │
│ actorAPI: ActorAPIClient │
│ }, │
│ │
│ // Request info │
│ request: { headers, ip }, │
│ │
│ // Per-request caching │
│ loaders: { │
│ movieLoader: DataLoader, │
│ actorLoader: DataLoader │
│ } │
│ } │
│ │
└─────────────────────────────────────────────────────────────────────┘
Context is created once per request and passed to every resolver.
Performance Considerations
The N+1 Problem
query {
movies { # 1 query: get all movies
title
director { # N queries: get director for each movie
name
}
}
}
Solution: DataLoader for batching (covered in implementation-specific tutorials).
Query Complexity
Deep or wide queries can be expensive:
query {
movies {
actors {
movies {
actors {
movies {
# ... deeply nested
}
}
}
}
}
}
Solutions:
- Query depth limiting
- Query complexity analysis
- Timeout limits
Step 4: Response Formation
After execution, results are assembled into the response:
{
"data": {
"movie": {
"title": "Inception",
"director": {
"name": "Christopher Nolan"
}
}
}
}
The response shape exactly mirrors the query shape.
With Errors
If errors occurred during execution:
{
"data": {
"movie": {
"title": "Inception",
"director": null
}
},
"errors": [{
"message": "Director service unavailable",
"path": ["movie", "director"]
}]
}
Extensions
Servers can include additional metadata in extensions:
{
"data": { ... },
"extensions": {
"tracing": {
"duration": 42,
"parsing": 2,
"validation": 5,
"execution": 35
},
"cacheControl": {
"maxAge": 300
}
}
}
Common uses:
- Performance tracing
- Cache hints
- Rate limit information
- Debug information (development only)
Summary
| Phase | Purpose | Errors |
|---|---|---|
| Parse | String → AST | Syntax errors |
| Validate | Check against schema | Validation errors |
| Execute | Resolve fields | Execution errors |
| Respond | Form JSON response | - |
What's Next?
In the next chapter, we'll explore Schema Design Best Practices - patterns and conventions for designing effective GraphQL schemas.