HomeCurriculumGraphQLGraphQL SDL Schemas, Queries & Mutations
GraphQL SDL Schemas, Queries & Mutations

GraphQL SDL Schemas, Queries & Mutations

Explore declarative data fetching, Schema Definition Language (SDL), AST query parsing, and exact client selection mechanics.

DT
Authored by DevThesis Systems Editorial Team
Reviewed for Technical Accuracy & Code Determinism

In 2012, as mobile smartphone adoption surged globally, engineering teams at Facebook confronted a critical performance bottleneck with their mobile application architecture. The mobile News Feed was sluggish, consumed excessive cellular data, and drained battery life. The root cause was not database latency, but the fundamental impedance mismatch between traditional RESTful API architectures and modern, multi-faceted client user interfaces.

To render a single mobile screen, the application had to execute multiple sequential HTTP requests (under-fetching / request waterfalls) or rely on bloated, monolithic endpoints that returned hundreds of unused database fields (over-fetching). In response, Facebook invented GraphQL—open-sourced in 2015—as an open-standard query language and execution engine for APIs.

Rather than exposing multiple endpoint URLs whose response shapes are fixed by the server, GraphQL exposes a single, strongly-typed endpoint where the client declares the exact shape, depth, and fields of the data it requires. This module provides an exhaustive, university-grade architectural analysis of GraphQL's type system, Schema Definition Language (SDL), query execution lifecycles, and mutation serial ordering semantics.


The Paradigm Shift: GraphQL vs REST#

Understanding GraphQL requires examining the core structural differences between resource-oriented network architectures (REST) and client-driven graph querying.

text
+-----------------------------------------------------------------------------------+
| Network Request Topologies: REST vs GraphQL                                       |
|                                                                                   |
|  REST Multi-Roundtrip Waterfall (Under-fetching):                                 |
|  Client                                Server Endpoints                           |
|    |                                          |                                   |
|    |--- 1. GET /v1/users/42 ----------------->| Returns User (1.8 KB JSON)        |
|    |<-- Response (id, name, email, etc.) -----|                                   |
|    |                                          |                                   |
|    |--- 2. GET /v1/users/42/posts ----------->| Returns Posts Array (14.2 KB JSON)|
|    |<-- Response ([{ id, title, ... }]) ------|                                   |
|    |                                          |                                   |
|    |--- 3. GET /v1/posts/101/comments ------->| Returns Comments (8.5 KB JSON)    |
|    |<-- Response ([{ id, text, ... }]) -------|                                   |
|    |                                          |                                   |
|    Total Network Latency = RTT1 + RTT2 + RTT3 (3 Round-trips across WAN)          |
|    Total Payload Received = 24.5 KB (Contains 70% unused metadata)                |
|                                                                                   |
|  -------------------------------------------------------------------------------  |
|                                                                                   |
|  GraphQL Single Query Execution (Declarative Fetching):                           |
|  Client                                Single Endpoint (/graphql)                 |
|    |                                          |                                   |
|    |--- POST /graphql ----------------------->| Single Request:                   |
|    |    query {                               | - Resolves User, Posts, Comments  |
|    |      user(id: "42") {                    |   concurrently in memory          |
|    |        name                              | - Trims all unrequested fields    |
|    |        posts(limit: 5) {                 |                                   |
|    |          title                           |                                   |
|    |          comments { text }               |                                   |
|    |        }                                 |                                   |
|    |      }                                   |                                   |
|    |    }                                     |                                   |
|    |<-- Exact Response Shape (1.9 KB) --------| Returns exact JSON hierarchy      |
|                                                                                   |
|    Total Network Latency = 1 RTT (Single Round-trip)                              |
|    Total Payload Received = 1.9 KB (Zero wasted bytes, 92.2% bandwidth reduction) |
+-----------------------------------------------------------------------------------+

Over-Fetching vs Under-Fetching#

  • Over-Fetching: Occurs when a client downloads more data than necessary to render a specific view. In a REST API, GET /v1/users/42 may return 45 attributes, including address, billing details, audit timestamps, and preference hashes, even though a navigation header component only needs displayName and avatarUrl. Over-fetching wastes bandwidth, increases JSON parsing CPU time on mobile devices, and strains battery life.
  • Under-Fetching (Waterfall Latency): Occurs when a single view requires data from multiple related resources. Because a REST endpoint represents an isolated resource, the client must wait for the first response before extracting foreign keys to issue subsequent requests. Over high-latency cellular networks (where Round-Trip Time RTT ≥ 100 ms), chaining three sequential network calls introduces an unavoidable 300+ ms latency floor regardless of backend database speed.

GraphQL permanently eliminates both issues: the client issues a single declarative query specifying only the fields required, and the server resolves the entire graph in a single network round-trip.


Schema Definition Language (SDL) & The Strong Type System#

A GraphQL API is strictly governed by its schema. The schema defines the complete capabilities of the API, serving as a type-safe contract between client and server. GraphQL schemas are written in Schema Definition Language (SDL).

text
+-----------------------------------------------------------------------------------+
| GraphQL Type System Taxonomy                                                      |
|                                                                                   |
|  +-----------------------------------------------------------------------------+  |
|  | Root Operation Types (Entry Points)                                         |  |
|  | - Query (Read operations)                                                   |  |
|  | - Mutation (Write operations with side effects)                             |  |
|  | - Subscription (Real-time push events via WebSockets)                       |  |
|  +-------------------------------------+---------------------------------------+  |
|                                        | Defines Operations                       |
|                                        v                                          |
|  +-----------------------------------------------------------------------------+  |
|  | Composite Types (Graph Nodes)                                               |  |
|  | - Object Types (Entities: User, Product, Order)                             |  |
|  | - Interfaces (Abstract contracts implemented by Object Types)               |  |
|  | - Union Types (Disjoint polymorphic collections: SearchResult)              |  |
|  +-------------------------------------+---------------------------------------+  |
|                                        | Contains Fields of                       |
|                                        v                                          |
|  +-----------------------------------------------------------------------------+  |
|  | Leaf Types (Graph Terminals)                                                |  |
|  | - Scalar Types (Int, Float, String, Boolean, ID, Custom Scalars)            |  |
|  | - Enum Types (Fixed set of discrete string constants: Status)               |  |
|  +-----------------------------------------------------------------------------+  |
|                                        ^                                          |
|                                        | Accepts Arguments As                     |
|  +-------------------------------------+---------------------------------------+  |
|  | Input Types                                                                 |  |
|  | - Input Object Types (input CreateUserInput { ... })                        |  |
|  | - Strict validation rule: Cannot contain fields with arguments or interfaces|  |
|  +-----------------------------------------------------------------------------+  |
+-----------------------------------------------------------------------------------+

1. Scalar Types: Built-In and Custom Scalars#

Scalars represent the leaf values of a GraphQL query. They resolve to concrete data values and cannot contain nested fields.

Built-in Primitive Scalars:#

  • Int: Signed 32-bit integer (-2^{31} to 2^{31} - 1).
  • Float: Signed double-precision fractional value (IEEE 754).
  • String: UTF-8 character sequence.
  • Boolean: true or false.
  • ID: A unique identifier serialized as a string. Indicates that the field is intended as a non-human-readable unique key (used by client caching engines for entity normalization).

Custom Scalars#

Real-world enterprise schemas require domain-specific data types that enforce validation rules:

graphql
scalar DateTime
scalar EmailAddress
scalar BigInt
scalar JSON

In JavaScript/TypeScript runtime engines, custom scalars define three core serialization methods:

  1. serialize(value): Coerces an internal backend object (e.g., JavaScript Date) into a JSON-compatible wire format (e.g., ISO-8601 string).
  2. parseValue(value): Converts an incoming JSON variable value into the internal backend representation before invoking resolvers.
  3. parseLiteral(ast): Validates and parses hardcoded inline AST literals in query strings.

2. Nullability and Null Bubbling Mechanics#

In GraphQL SDL, all fields are nullable by default. A field is declared non-nullable by appending an exclamation mark (!):

  • String: May return a string or null.
  • String!: Must always return a string; null is illegal.
  • [String]: The list itself may be null, and items within the list may be null (e.g., ["a", null, "b"] or null).
  • [String!]: The list itself may be null, but items cannot be null (e.g., ["a", "b"] or null).
  • [String!]!: The list cannot be null, and no items within the list can be null (e.g., ["a", "b"] or []).
text
+-----------------------------------------------------------------------------------+
| The Null Bubbling Cascade Principle                                               |
|                                                                                   |
|  Schema Definition:                                                               |
|  type Query {                                                                     |
|    user(id: ID!): User                                                            |
|  }                                                                                |
|  type User {                                                                      |
|    id: ID!                                                                        |
|    profile: Profile!  <-- Non-nullable field!                                     |
|  }                                                                                |
|  type Profile {                                                                   |
|    bio: String!       <-- Non-nullable field!                                     |
|  }                                                                                |
|                                                                                   |
|  Runtime Execution Failure:                                                       |
|  1. Database error causes Profile.bio resolver to throw an exception / return null.|
|  2. Profile.bio is non-nullable (String!), so the null violates the invariant.     |
|  3. The error bubbles up to the parent: Profile is forced to become null.         |
|  4. However, User.profile is ALSO non-nullable (Profile!).                        |
|  5. The null bubbles up again: User is forced to become null.                     |
|  6. Query.user is nullable (User), so bubbling stops at the root.                 |
|                                                                                   |
|  Resulting Payload:                                                               |
|  {                                                                                |
|    "data": { "user": null },                                                      |
|    "errors": [{ "message": "Cannot return null for non-nullable field Profile.bio"|
|  }                                                                                |
|  An entire sub-tree was obliterated due to aggressive non-nullability!           |
+-----------------------------------------------------------------------------------+

The Semantic Nullability Trade-off#

While non-nullability provides pleasant TypeScript typing on the frontend (eliminating if (user?.profile?.bio) checks), it introduces severe operational fragility. If a microservice fails or a single non-critical database column holds null, null bubbling wipes out parent objects.

Production Best Practice: Use non-nullable fields (!) strictly for identifiers (id: ID!) and mandatory input arguments. Keep domain object relationships nullable unless the parent object has zero semantic validity without that child.

3. Abstract Types: Interfaces and Union Types#

GraphQL supports polymorphism through Interfaces and Union Types.

Interfaces#

An interface defines an abstract set of fields that implementing object types must include:

graphql
interface Node {
  id: ID!
}

interface ContentItem {
  id: ID!
  title: String!
  publishedAt: DateTime!
  author: User!
}

type Article implements ContentItem & Node {
  id: ID!
  title: String!
  publishedAt: DateTime!
  author: User!
  wordCount: Int!
  bodyHtml: String!
}

type Video implements ContentItem & Node {
  id: ID!
  title: String!
  publishedAt: DateTime!
  author: User!
  durationSeconds: Int!
  videoUrl: String!
}

Union Types#

A union type represents an entity that could be one of several distinct object types without requiring shared fields:

graphql
union SearchResult = Article | Video | User

Polymorphic Querying via Inline Fragments#

To query fields specific to concrete types, clients utilize Inline Fragments and the special __typename meta-field:

graphql
query GlobalSearch(term: String!) {
  search(query: term) {
    __typename
    ... on Article {
      id
      title
      wordCount
    }
    ... on Video {
      id
      title
      durationSeconds
    }
    ... on User {
      id
      displayName
      avatarUrl
    }
  }
}

Anatomy of GraphQL Operations#

A GraphQL document sent over HTTP consists of three primary operation types: query, mutation, and subscription.

1. Queries (Read Operations)#

Queries represent side-effect-free data retrieval. Fields within a query execution set are executed concurrently/in parallel by the runtime engine whenever possible.

graphql
query FetchUserDashboard(userId: ID!, includeTelemetry: Boolean = false) {
  user(id: userId) {
    id
    fullName
    email
    orders(limit: 5) {
      id
      totalCents
      status
    }
    telemetry @include(if: includeTelemetry) {
      lastLoginIp
      failedLoginAttempts
    }
  }
}

Key features:

  • Operation Name (FetchUserDashboard): Mandatory for production observability, tracing, and logging in tools like Apollo Studio and Datadog.
  • Variable Definitions (userId: ID!): Type-checked parameters preventing query string concatenation attacks (the GraphQL equivalent of SQL injection).
  • Directives (@include(if: ...), @skip(if: ...)): Conditionally include or exclude fields based on runtime boolean variables.
  • Field Aliases: Rename fields in the response to resolve naming collisions:
graphql
query CompareProfiles {
  primaryUser: user(id: "1") { name }
  secondaryUser: user(id: "2") { name }
}

2. Mutations (Write Operations & Side Effects)#

Mutations modify server-side state.

Critical Execution Invariant: Serial Execution#

While fields in a query execute in parallel, top-level fields in a mutation must execute serially, in the exact order they are declared in the query document:

graphql
mutation ExecuteSequencedOrders {
  firstOrder: createOrder(input: { sku: "ITEM-A" }) { id status }
  secondOrder: createOrder(input: { sku: "ITEM-B" }) { id status }
}

The GraphQL specification mandates that firstOrder must completely finish execution (and commit any database transactions) before secondOrder begins. This prevents data race conditions and deadlocks when mutations depend on sequential state changes.

Mutation Payload Best Practice (Relay Mutation Specification)#

Never return simple scalar booleans (createOrder: Boolean) from a mutation. Always accept a single, structured input object and return a dedicated payload type that includes:

  1. The mutated entity (for client cache normalization).
  2. Domain-specific, structured validation errors.
graphql
input CreateOrderInput {
  customerId: ID!
  items: [OrderItemInput!]!
  clientMutationId: String
}

type OrderError {
  field: String
  message: String!
  code: OrderErrorCode!
}

type CreateOrderPayload {
  order: Order
  errors: [OrderError!]!
  clientMutationId: String
}

type Mutation {
  createOrder(input: CreateOrderInput!): CreateOrderPayload!
}

The Query Execution Lifecycle & AST Pipeline#

When an HTTP POST request containing a GraphQL query arrives at the server, it passes through three rigorous compiler phases before data is fetched:

text
  [ Incoming HTTP POST Payload: { "query": "...", "variables": { ... } } ]
                                      |
                                      v
  +-----------------------------------------------------------------------------+
  | Phase 1: Lexical Analysis & Parsing                                         |
  | - Lexer tokenizes raw query string.                                         |
  | - Parser constructs Abstract Syntax Tree (AST).                             |
  | - Detects syntax errors (e.g., missing closing braces).                     |
  +-------------------------------------+---------------------------------------+
                                        | Valid AST
                                        v
  +-----------------------------------------------------------------------------+
  | Phase 2: Schema Validation & Security Inspection                            |
  | - AST is validated against the GraphQL Schema definition.                   |
  | - Verifies: Do fields exist? Are arguments valid? Are types compatible?    |
  | - Security checks: Calculates query depth and complexity limits.            |
  | - Detects fragment cycles (e.g., Fragment A spreads B, B spreads A).        |
  +-------------------------------------+---------------------------------------+
                                        | Validated & Safe AST
                                        v
  +-----------------------------------------------------------------------------+
  | Phase 3: Runtime Execution & Field Resolution                               |
  | - Traverses AST nodes top-down starting at Root Query / Mutation.           |
  | - Dispatches field resolvers (parent, args, context, info).                 |
  | - Batches field database lookups via DataLoader.                            |
  | - Handles null bubbling and formats JSON error arrays.                      |
  +-------------------------------------+---------------------------------------+
                                        |
                                        v
  [ HTTP Response Payload: { "data": { ... }, "errors": [ ... ] } ]

Common Pitfalls & Anti-Patterns#

1. The Cyclic Graph DoS Vulnerability#

Because GraphQL allows nested queries, a malicious user can construct a recursive query that causes exponential execution complexity on the server:

graphql
query MaliciousDoS {
  user(id: "1") {
    posts {
      author {
        posts {
          author {
            posts {
              author {
                # Infinite recursion crashing server RAM and CPU
              }
            }
          }
        }
      }
    }
  }
}
  • Remedy: Enforce Query Depth Limiting (e.g., max depth = 6) and Query Complexity Analysis at validation time before resolvers execute.

2. Overuse of Non-Nullability Leading to Catastrophic Cascades#

Marking all fields with ! causes minor errors in subordinate microservices to wipe out the entire response payload via null bubbling.

  • Remedy: Make leaf scalars non-null if strictly required, but keep relations and complex object types nullable.

3. Exposing Database Schemas 1:1 in GraphQL SDL#

Directly mirroring PostgreSQL or MongoDB collections into GraphQL types tightly couples the API to the database layout, preventing clean schema evolution.

  • Remedy: Model GraphQL schemas around Client User Journeys and Business Capabilities, maintaining an abstraction layer between resolvers and raw database tables.

Checkpoint Practice Problems#

Test your mastery of GraphQL SDL, null bubbling mechanics, AST analysis, and schema design through these detailed engineering scenarios.

Problem 1: Network Payload Over-fetching vs GraphQL Savings Analysis#

A mobile news reader application renders an article feed. Each feed item card displays:

  • Article ID (id: 16 bytes)
  • Headline (title: 64 bytes)
  • Author Display Name (authorName: 32 bytes)
  • Thumbnail URL (thumbnailUrl: 128 bytes)

Total required data per card = 240 bytes.

In a legacy REST architecture, the mobile client calls GET /v1/articles?limit=20.

The REST endpoint serializes the full database entity for each article, returning 38 attributes per article (including full markdown body, revision history, internal editorial flags, and raw tags), averaging S_{rest} = 4,200 bytes of JSON per article.

In the GraphQL architecture, the client issues:

graphql
query GetFeed {
  articles(limit: 20) {
    id
    title
    author { displayName }
    thumbnailUrl
  }
}

The resulting GraphQL JSON payload consumes S_{graphql} = 290 bytes per article (accounting for JSON formatting keys).

Assume the mobile application has 2,000,000 active daily users, each loading the feed an average of 15 times per day.

Calculate:

  1. The total daily network bandwidth consumed under the REST architecture in Terabytes (1 TB = 10^{12} bytes).
  2. The total daily network bandwidth consumed under the GraphQL architecture in Terabytes.
  3. Compute the total monthly bandwidth savings (30-day month) and calculate the financial cost reduction assuming cellular CDN egress pricing of \0.065 per Gigabyte (1 GB = 10^9 bytes).

Solution:

Step 1: Compute Total Daily Requests:

Total Daily Requests = 2,000,000 users × 15 loads/user = 30,000,000 requests/dayStep 2: Bandwidth Consumption under REST:

Each request fetches 20 articles:

Payload per request (REST) = 20 × 4,200 bytes = 84,000 bytes = 84 KBDaily data transferred:

Daily Bytes (REST) = 30,000,000 × 84,000 bytes = 2.52 × 10^{12} bytes = 2.52 TB/dayStep 3: Bandwidth Consumption under GraphQL:

Payload per request (GraphQL) = 20 × 290 bytes = 5,800 bytes = 5.8 KBDaily data transferred:

Daily Bytes (GraphQL) = 30,000,000 × 5,800 bytes = 1.74 × 10^{11} bytes = 0.174 TB/dayStep 4: Monthly Financial Savings Analysis:

Daily Bandwidth Saved = 2.52 TB - 0.174 TB = 2.346 TB/day = 2,346 GB/day.

Monthly Bandwidth Saved (30 days) = 2,346 × 30 = 70,380 GB/month.

Percentage Bandwidth Reduction:

Reduction = ≤ft(1 - (0.174) / (2.52)) × 100\% = 93.10\% reductionMonthly Egress Cost Savings:

Monthly Cost Savings = 70,380 GB × \0.065 = \4,574.70 per monthBeyond saving \4,574/month in CDN egress, reducing payload sizes from 84 KB to 5.8 KB allows responses to fit within the initial TCP congestion window (Initial Congestion Window initcwnd ≈ 10 packets ≈ 14.6 KB), eliminating TCP round-trip packet ACKs and reducing mobile load latency by over 60%.


Problem 2: Null Bubbling Error Cascade Derivation#

Examine the following GraphQL schema and query:

graphql
type Query {
  organization(id: ID!): Organization!
}

type Organization {
  id: ID!
  name: String!
  headquarters: Location!
  departments: [Department!]!
}

type Location {
  city: String!
  country: String!
  coordinates: GeoCoordinates!
}

type GeoCoordinates {
  latitude: Float!
  longitude: Float!
}

type Department {
  id: ID!
  manager: Employee!
}

type Employee {
  id: ID!
  salaryCents: Int!
}

A client executes:

graphql
query GetOrgData {
  organization(id: "org_99") {
    name
    headquarters {
      city
      coordinates {
        latitude
        longitude
      }
    }
    departments {
      id
      manager {
        id
        salaryCents
      }
    }
  }
}

During execution:

  • Case A: The resolver for GeoCoordinates.latitude throws an unhandled database exception (Column 'lat' is null).
  • Case B: For Department #3 in the list of 5 departments, the employee manager was deleted from the HR system, causing Department.manager to return null.

Trace the exact null bubbling path for both cases, specify the resulting data object returned to the client, and identify which fields survive.

Solution:

Analysis of Case A (`GeoCoordinates.latitude` throws null):

  1. GeoCoordinates.latitude is defined as Float! (non-nullable). The return of null is a schema violation.
  2. The error bubbles to the parent object: Location.coordinates.
  3. Location.coordinates is defined as GeoCoordinates! (non-nullable). It cannot accept null.
  4. The error bubbles to its parent object: Organization.headquarters.
  5. Organization.headquarters is defined as Location! (non-nullable). It cannot accept null.
  6. The error bubbles to its parent object: Query.organization.
  7. Query.organization is defined as Organization! (non-nullable). It cannot accept null.
  8. The error bubbles to the root Query execution set.
  9. Result for Case A:

The entire response payload is destroyed:

```json

{

"data": null,

"errors": [

{

"message": "Cannot return null for non-nullable field GeoCoordinates.latitude.",

"path": ["organization", "headquarters", "coordinates", "latitude"]

}

]

}

```

Even though organization.name and all departments resolved successfully, the non-null chain on latitude resulted in a total response blackout.

Analysis of Case B (`Department.manager` returns null on department #3):

  1. For department #3, Department.manager is defined as Employee! (non-nullable). Returning null violates the invariant.
  2. The error bubbles to the parent: the specific Department element at index 2 in departments.
  3. Organization.departments is defined as [Department!]! (a non-nullable list of non-nullable items).
  4. Because the array items are non-nullable (Department!), an array cannot contain a null item (e.g., [dept0, dept1, null, dept3, dept4] is illegal).
  5. The error forces the entire departments list to become null.
  6. However, departments is ALSO declared as [Department!]! (non-nullable list). It cannot be null.
  7. The error bubbles to Organization.departments.
  8. Query.organization is Organization!.
  9. Result for Case B:

The error bubbles all the way to root again:

```json

{

"data": null,

"errors": [

{

"message": "Cannot return null for non-nullable field Department.manager.",

"path": ["organization", "departments", 2, "manager"]

}

]

}

```

Architectural Takeaway: If departments had been defined as [Department!] (nullable list) or if manager had been defined as Employee (nullable field), department #3 would simply report manager: null while the rest of the organization payload remained completely intact!


Problem 3: Query AST Depth & Complexity Calculation Engine#

Implement a complete, production-grade TypeScript AST validation rule that traverses an incoming GraphQL query DocumentAST, computes the exact maximum nesting depth, and rejects queries exceeding a configurable threshold (maxDepth = 5), preventing recursive DoS attacks.

Solution:

typescript
// depth-limiter.ts
import {
  ValidationContext,
  ASTVisitor,
  FieldNode,
  InlineFragmentNode,
  FragmentSpreadNode,
  OperationDefinitionNode,
  GraphQLError
} from 'graphql';

export function createDepthLimitRule(maxDepth: number) {
  return function depthLimitValidationRule(context: ValidationContext): ASTVisitor {
    return {
      OperationDefinition(node: OperationDefinitionNode) {
        const depth = calculateNodeDepth(node, 0, context);
        if (depth > maxDepth) {
          context.reportError(
            new GraphQLError(
              `Operation '{node.name?.value || 'anonymous'}' exceeds maximum permitted query depth of {maxDepth}. Detected depth: {depth}.`,
              { nodes: [node] }
            )
          );
        }
      }
    };
  };
}

function calculateNodeDepth(node: any, currentDepth: number, context: ValidationContext): number {
  if (!node.selectionSet || !node.selectionSet.selections) {
    return currentDepth;
  }

  let maxChildDepth = currentDepth;

  for (const selection of node.selectionSet.selections) {
    let childDepth = currentDepth;

    if (selection.kind === 'Field') {
      const fieldNode = selection as FieldNode;
      // Introspection fields (__schema, __typename) do not count toward depth
      if (fieldNode.name.value.startsWith('__')) {
        continue;
      }
      childDepth = calculateNodeDepth(fieldNode, currentDepth + 1, context);
    } else if (selection.kind === 'InlineFragment') {
      const inlineFrag = selection as InlineFragmentNode;
      // Inline fragments don't increase depth on their own
      childDepth = calculateNodeDepth(inlineFrag, currentDepth, context);
    } else if (selection.kind === 'FragmentSpread') {
      const spreadNode = selection as FragmentSpreadNode;
      const fragment = context.getFragment(spreadNode.name.value);
      if (fragment) {
        childDepth = calculateNodeDepth(fragment, currentDepth, context);
      }
    }

    if (childDepth > maxChildDepth) {
      maxChildDepth = childDepth;
    }
  }

  return maxChildDepth;
}

Through this AST visitor rule, queries are inspected statically during Phase 2 (Validation) before any database resolvers or network calls are triggered. If a query's recursive field depth exceeds 5, the execution engine instantaneously rejects it with an informative GraphQLError.


Problem 4: Complete Production GraphQL SDL Schema & Mutation Design#

Construct an enterprise GraphQL Schema Definition Language (SDL) schema for an e-commerce platform incorporating:

  1. Custom DateTime and PositiveInt scalars.
  2. A polymorphic SearchResult union combining Product, Category, and Brand.
  3. An OrderStatus enum.
  4. A complete Relay-style cursor connection pagination type for OrderConnection.
  5. A mutation for checkoutOrder adhering to the Relay mutation design pattern with explicit error unions.

Solution:

graphql
# schema.graphql

# An ISO-8601 compliant datetime string.
scalar DateTime

# An integer strictly greater than zero.
scalar PositiveInt

enum OrderStatus {
  PENDING_PAYMENT
  PAYMENT_CONFIRMED
  PROCESSING
  DISPATCHED
  DELIVERED
  CANCELLED
}

interface Node {
  id: ID!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPrevPage: Boolean!
  startCursor: String
  endCursor: String
}

type Product implements Node {
  id: ID!
  sku: String!
  name: String!
  priceCents: PositiveInt!
  inStock: Boolean!
  createdAt: DateTime!
}

type Category implements Node {
  id: ID!
  slug: String!
  displayName: String!
  productCount: Int!
}

type Brand implements Node {
  id: ID!
  name: String!
  websiteUrl: String
}

union SearchResult = Product | Category | Brand

type OrderItem {
  product: Product!
  quantity: PositiveInt!
  unitPriceCents: PositiveInt!
}

type Order implements Node {
  id: ID!
  orderNumber: String!
  status: OrderStatus!
  items: [OrderItem!]!
  totalCents: PositiveInt!
  createdAt: DateTime!
}

type OrderEdge {
  cursor: String!
  node: Order!
}

type OrderConnection {
  edges: [OrderEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

# --- MUTATION INPUTS & PAYLOADS ---

input CartItemInput {
  productId: ID!
  quantity: PositiveInt!
}

input CheckoutOrderInput {
  customerId: ID!
  items: [CartItemInput!]!
  shippingAddressId: ID!
  clientMutationId: String
}

enum CheckoutErrorCode {
  OUT_OF_STOCK
  INVALID_PAYMENT_METHOD
  INSUFFICIENT_FUNDS
  CUSTOMER_SUSPENDED
}

type CheckoutUserError {
  field: String
  message: String!
  code: CheckoutErrorCode!
}

type CheckoutOrderPayload {
  order: Order
  userErrors: [CheckoutUserError!]!
  clientMutationId: String
}

# --- ROOT OPERATIONS ---

type Query {
  node(id: ID!): Node
  product(id: ID!): Product
  search(query: String!, limit: Int = 10): [SearchResult!]!
  orders(
    first: Int
    after: String
    status: OrderStatus
  ): OrderConnection!
}

type Mutation {
  checkoutOrder(input: CheckoutOrderInput!): CheckoutOrderPayload!
}

This schema exemplifies enterprise GraphQL architectural principles: robust polymorphism via union SearchResult, standardized cursor pagination via OrderConnection, clean Relay-style mutation contracts with explicit typed domain errors (userErrors), and resilient nullability constraints.

Key Architectural Takeaways

  • GraphQL decouples UI components from rigid endpoint contracts via client-specified selection sets.
  • The SDL codifies entities into a strictly typed directed graph of scalars, object types, and modifiers.
  • Query execution encompasses lexical tokenization, AST generation, schema validation, and field resolution.

Hands-on Code Playground Available

Execute, edit, and experiment with code for this topic in the live sandbox.

Practice Check • Quick Quiz

Test Your Understanding

What is the primary architectural benefit of GraphQL client-specified field selection compared to traditional REST?

A
It automatically indexes database tables in memory.
B
It completely eliminates over-fetching and under-fetching by shaping responses to match client queries.
C
It replaces TCP with UDP for transport.
D
It compiles server code into WebAssembly.