Table of Contents

    GraphQL

    API DESIGN & SERVICE CONTRACTS

    GraphQL

    Learn how GraphQL APIs use strongly typed schemas, client-defined selection sets, queries, mutations, variables, resolvers, pagination, authorization and operation limits to provide flexible service contracts.

    Introduction

    GraphQL is a query language and execution model for APIs. A GraphQL service publishes a typed schema describing the data and operations available to clients.

    Instead of selecting from many resource-specific endpoints, a GraphQL client submits an operation describing the fields it needs. The server validates that operation against the schema, executes the corresponding field resolvers and returns a response shaped similarly to the client's selection.

    GraphQL commonly provides:

    • A strongly typed API schema
    • Client-defined response selections
    • Queries for reading data
    • Mutations for changing server-side data
    • Subscriptions for event-driven updates where supported
    • Nested relationships between types
    • Variables and reusable fragments
    • Schema introspection
    • Validation before operation execution
    • Structured data and error responses

    Core idea: The GraphQL schema defines what clients are allowed to request, while each operation defines which permitted fields the client wants in that response.

    In your System Design curriculum, GraphQL is Topic 4.3 under API Design & Service Contracts. It follows REST and RPC with gRPC and precedes resource modelling, pagination, versioning, idempotency, authentication versus authorization and rate-limit semantics.

    Prerequisites

    # Prerequisite Why It Is Needed
    1 REST APIs GraphQL solves several API-consumption problems through a different contract model.
    2 RPC and gRPC GraphQL can be compared with method-oriented and resource-oriented API designs.
    3 HTTP and JSON GraphQL operations are commonly transported using HTTP and return JSON responses.
    4 Database relationships Nested GraphQL fields commonly represent relationships between entities.
    5 Authentication and authorization Every protected field and object requires appropriate access control.
    6 Pagination and caching basics GraphQL collections and client-selected responses require deliberate performance controls.

    What Is GraphQL?

    GraphQL describes the data and operations available from an API through a schema. Clients submit operations against the schema, and the service returns predictable results based on the selected fields.

    GraphQL Request Lifecycle
    receive operation → parse document → validate against schema → authorize access → execute resolvers → return selected fields

    Example Query

    query GetCustomer {
      customer(id: "42") {
        id
        name
        status
      }
    }

    Example Response

    {
      "data": {
        "customer": {
          "id": "42",
          "name": "Example Customer",
          "status": "ACTIVE"
        }
      }
    }

    The fields in the response correspond to the fields selected by the client.

    GraphQL vs REST vs gRPC

    Area REST GraphQL gRPC
    Primary abstraction Resources Typed graph of fields Services and remote methods
    Request structure HTTP method, URI and representation Operation and selection set Typed RPC request message
    Response shape Defined by each endpoint Selected by the client within schema limits Defined by the method response type
    Common payload JSON JSON Protocol Buffers
    Browser integration Natural HTTP integration Natural HTTP integration Commonly requires gRPC-Web or a gateway
    Contract Endpoint documentation or OpenAPI GraphQL schema Protocol Buffer service definition
    Caching Can use standard HTTP resource caching naturally Often requires client normalization or operation-aware caching Normally application or infrastructure controlled

    GraphQL Schema

    A GraphQL schema describes the capabilities of the service. It defines the available types, fields, relationships, arguments and root operations.

    type Customer {
      id: ID!
      name: String!
      email: String
      status: CustomerStatus!
      orders(
        first: Int = 20
        after: String
      ): OrderConnection!
    }
    
    enum CustomerStatus {
      ACTIVE
      INACTIVE
    }
    
    type Order {
      id: ID!
      status: OrderStatus!
      total: Float!
      customer: Customer!
    }
    
    enum OrderStatus {
      PENDING
      CONFIRMED
      PROCESSING
      COMPLETED
      CANCELLED
    }
    
    type Query {
      customer(id: ID!): Customer
      order(id: ID!): Order
      customers(
        first: Int = 20
        after: String
      ): CustomerConnection!
    }
    
    type Mutation {
      createCustomer(
        input: CreateCustomerInput!
      ): CreateCustomerPayload!
    
      updateCustomer(
        input: UpdateCustomerInput!
      ): UpdateCustomerPayload!
    }

    The schema is a public contract. Renaming a field, changing its type or tightening its nullability can break existing clients.

    GraphQL Named Types

    Type Purpose
    Scalar Represents a leaf value such as String, Int, Float, Boolean or ID
    Object Represents an entity with selectable fields
    Interface Defines fields shared by several implementing object types
    Union Represents one value from several possible object types
    Enum Restricts a value to a documented set of symbolic values
    Input Object Groups structured arguments supplied to a field or mutation

    Scalar Types

    type Product {
      id: ID!
      name: String!
      description: String
      quantity: Int!
      price: Float!
      available: Boolean!
    }

    Custom scalar types can represent domain values such as dates, timestamps, decimal amounts and URLs.

    scalar DateTime
    scalar Decimal
    
    type Invoice {
      id: ID!
      issuedAt: DateTime!
      amount: Decimal!
    }

    Every custom scalar needs documented serialization, parsing and validation behaviour.

    Nullability

    An exclamation mark indicates a non-null type.

    type Customer {
      id: ID!
      name: String!
      email: String
    }
    Type Meaning
    String The value can be a string or null
    String! The value must be a string and cannot be null
    [String] The list and its items can be null
    [String!] The list can be null, but each item must be non-null
    [String!]! The list and every item must be non-null

    Compatibility warning: Nullability is part of the service contract. Mark a field as non-null only when the server can reliably produce that value for every valid object.

    Queries

    A query reads data. The client selects fields from the schema's Query root.

    query GetOrder {
      order(id: "9001") {
        id
        status
        total
        customer {
          id
          name
        }
      }
    }
    {
      "data": {
        "order": {
          "id": "9001",
          "status": "CONFIRMED",
          "total": 149.50,
          "customer": {
            "id": "42",
            "name": "Example Customer"
          }
        }
      }
    }

    A query can traverse relationships within one operation, but the server must control the resulting execution cost.

    Operation Names

    Operations should have meaningful names for logging, debugging, analytics and persisted-operation management.

    Anonymous production operation
    {
      customer(id: "42") {
        id
        name
      }
    }
    Named operation
    query GetCustomerSummary {
      customer(id: "42") {
        id
        name
      }
    }

    Variables

    Variables separate dynamic input values from the GraphQL operation text.

    Operation

    query GetCustomer(
      $customerId: ID!
    ) {
      customer(id: $customerId) {
        id
        name
        email
        status
      }
    }

    Variables

    {
      "customerId": "42"
    }

    Variables improve operation reuse and allow input values to be validated against declared GraphQL types.

    Arguments

    Fields can accept arguments that control lookup, filtering, sorting or pagination.

    query GetConfirmedOrders {
      orders(
        status: CONFIRMED
        first: 20
        after: null
      ) {
        nodes {
          id
          total
          status
        }
      }
    }

    The schema should define supported arguments, default values, validation constraints and maximum limits.

    Aliases

    Aliases allow a client to request the same field several times with different arguments.

    query CompareOrders {
      firstOrder: order(id: "9001") {
        id
        status
      }
    
      secondOrder: order(id: "9002") {
        id
        status
      }
    }
    {
      "data": {
        "firstOrder": {
          "id": "9001",
          "status": "CONFIRMED"
        },
        "secondOrder": {
          "id": "9002",
          "status": "PROCESSING"
        }
      }
    }

    Fragments

    Fragments define reusable field selections.

    fragment CustomerSummary on Customer {
      id
      name
      status
    }
    
    query GetCustomerAndOrder {
      customer(id: "42") {
        ...CustomerSummary
      }
    
      order(id: "9001") {
        id
        customer {
          ...CustomerSummary
        }
      }
    }

    Fragments reduce repeated client-side operation definitions. They do not automatically reduce server execution cost.

    Directives

    Directives can conditionally include or skip selected fields.

    query GetCustomer(
      $customerId: ID!
      $includeEmail: Boolean!
    ) {
      customer(id: $customerId) {
        id
        name
        email @include(if: $includeEmail)
      }
    }

    Custom directives can also support implementation-specific schema behaviour, but their meaning must be documented clearly.

    Mutations

    Mutations represent operations that can change server-side state. Side effects should be associated with top-level mutation fields.

    Mutation Schema

    input CreateCustomerInput {
      name: String!
      email: String!
      idempotencyKey: String!
    }
    
    type CreateCustomerPayload {
      customer: Customer
      errors: [UserError!]!
    }
    
    type UserError {
      field: String
      code: String!
      message: String!
    }
    
    type Mutation {
      createCustomer(
        input: CreateCustomerInput!
      ): CreateCustomerPayload!
    }

    Mutation Operation

    mutation CreateCustomer(
      $input: CreateCustomerInput!
    ) {
      createCustomer(input: $input) {
        customer {
          id
          name
          email
          status
        }
        errors {
          field
          code
          message
        }
      }
    }

    Mutation Variables

    {
      "input": {
        "name": "Example Customer",
        "email": "customer@example.com",
        "idempotencyKey": "create-customer-1042"
      }
    }

    Returning the affected object allows the client to obtain its latest server-controlled fields after the mutation.

    Idempotent Mutations

    A network timeout can occur after a mutation has completed. Mutations that support safe retries should accept an idempotency key or another stable operation identifier.

    First request with key K:
    
    Validate request
    Create customer
    Store result for K
    Return result
    
    
    Retry with the same key and input:
    
    Find stored result
    Do not create another customer
    Return original result
    
    
    Same key with different input:
    
    Reject the request as a conflict

    Subscriptions

    A GraphQL subscription represents an operation whose result can be updated over time.

    type Subscription {
      orderUpdated(
        orderId: ID!
      ): OrderUpdate!
    }
    
    type OrderUpdate {
      orderId: ID!
      version: Int!
      status: OrderStatus!
      occurredAt: String!
    }
    subscription WatchOrder(
      $orderId: ID!
    ) {
      orderUpdated(
        orderId: $orderId
      ) {
        orderId
        version
        status
        occurredAt
      }
    }

    The GraphQL schema defines the subscription operation, while the implementation must select a compatible transport and event-delivery model.

    A subscription design should define:

    • Transport protocol
    • Connection authentication
    • Per-subscription authorization
    • Event ordering
    • Event identifiers
    • Reconnect behaviour
    • Missed-event recovery
    • Backpressure
    • Subscription limits

    Resolvers

    A resolver is application logic responsible for producing the value of a GraphQL field.

    GraphQL operation
          |
          v
    Query.customer resolver
          |
          v
    Customer.name resolver
    Customer.orders resolver
          |
          v
    Database, cache or service calls
          |
          v
    GraphQL response

    Resolvers can retrieve data from:

    • Relational databases
    • Document databases
    • Caches
    • REST APIs
    • gRPC services
    • Message or event systems
    • In-memory application state

    Conceptual PHP Resolver

    <?php
    
    declare(strict_types=1);
    
    function resolveCustomer(
        mixed $root,
        array $arguments,
        array $context
    ): ?array {
        $customerId =
            $arguments['id'] ?? null;
    
        if (!is_string($customerId) ||
            $customerId === '') {
    
            throw new InvalidArgumentException(
                'A customer ID is required.'
            );
        }
    
        $identity =
            $context['identity'] ?? null;
    
        if ($identity === null) {
            throw new RuntimeException(
                'Authentication is required.'
            );
        }
    
        $customer =
            findCustomerById(
                $customerId
            );
    
        if ($customer === null) {
            return null;
        }
    
        if (!canViewCustomer(
                $identity,
                $customer
            )) {
    
            throw new RuntimeException(
                'Access is denied.'
            );
        }
    
        return $customer;
    }

    The framework should translate internal exceptions into a safe GraphQL error contract without exposing stack traces or database details.

    The N+1 Query Problem

    Nested fields can accidentally generate one database query for the parent collection and one additional query for every returned item.

    query GetCustomers {
      customers(first: 100) {
        nodes {
          id
          name
          orders {
            nodes {
              id
              status
            }
          }
        }
      }
    }
    One query:
    
    Load 100 customers.
    
    
    Then 100 additional queries:
    
    Load orders for customer 1.
    Load orders for customer 2.
    Load orders for customer 3.
    ...
    Load orders for customer 100.
    
    
    Total:
    
    1 + 100 database queries

    Batching Solution

    Collect requested customer IDs:
    
    1, 2, 3, ..., 100
    
    
    Execute one batched order query:
    
    Load orders where customer ID
    belongs to the collected set.
    
    
    Group results by customer ID.

    Request-scoped batching and caching mechanisms can consolidate repeated resolver access while preserving authorization and data-isolation rules.

    Pagination

    List fields should use bounded pagination rather than returning unlimited collections.

    type CustomerConnection {
      edges: [CustomerEdge!]!
      nodes: [Customer!]!
      pageInfo: PageInfo!
    }
    
    type CustomerEdge {
      cursor: String!
      node: Customer!
    }
    
    type PageInfo {
      endCursor: String
      hasNextPage: Boolean!
    }
    
    type Query {
      customers(
        first: Int = 20
        after: String
      ): CustomerConnection!
    }

    Cursor Query

    query GetCustomers(
      $first: Int!
      $after: String
    ) {
      customers(
        first: $first
        after: $after
      ) {
        nodes {
          id
          name
          status
        }
        pageInfo {
          endCursor
          hasNextPage
        }
      }
    }

    Pagination rule: Define a stable ordering before creating a cursor. The cursor should be opaque to clients and should not be treated as an editable page number.

    Authentication

    Authentication should occur before protected resolver execution. The established identity can be placed in a request-scoped execution context.

    HTTP request arrives
          |
          v
    Validate authorization credential
          |
          v
    Create request identity context
          |
          v
    Parse and validate GraphQL operation
          |
          v
    Resolvers use trusted identity context

    A resolver should not trust an identity, account or tenant identifier merely because the client supplied it as an argument.

    Field and Object Authorization

    Authorization can be required at several levels:

    • Operation-level authorization
    • Root field authorization
    • Object-level authorization
    • Field-level authorization
    • Relationship authorization
    • Mutation-specific authorization
    query GetCustomer {
      customer(id: "42") {
        id
        name
        email
        internalRiskScore
      }
    }
    Required checks:
    
    1. Can the caller access customer 42?
    2. Can the caller view the email field?
    3. Can the caller view internalRiskScore?
    4. Can the resolver access related records?
    5. Does the caller belong to the correct tenant?

    Authorization rule: A field appearing in the GraphQL schema does not mean every authenticated caller is allowed to retrieve it.

    Query Depth and Complexity

    Flexible nested queries can create expensive execution plans.

    query ExpensiveOperation {
      customers(first: 100) {
        nodes {
          orders(first: 100) {
            nodes {
              items(first: 100) {
                nodes {
                  product {
                    supplier {
                      products(first: 100) {
                        nodes {
                          id
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }

    GraphQL services should apply controls such as:

    • Maximum operation depth
    • Maximum selected field count
    • Weighted field complexity
    • Maximum list arguments
    • Maximum aliases
    • Maximum operation document size
    • Maximum variable size
    • Resolver deadlines
    • Request rate limits

    Conceptual Complexity Model

    Operation cost
    =
    Scalar-field costs
    +
    Object-field costs
    +
    Requested list size multipliers
    +
    Expensive resolver weights

    The model should reflect actual resolver and data-source cost rather than relying only on syntax depth.

    Timeouts and Cancellation

    GraphQL execution should use a finite request deadline and propagate cancellation to database and downstream service calls.

    Client cancels request
            |
            v
    HTTP request context is cancelled
            |
            v
    GraphQL execution observes cancellation
            |
            v
    Resolvers stop unnecessary work
            |
            v
    Database and downstream calls are cancelled

    Cancellation is cooperative. A mutation might already have committed its business operation before cancellation is observed.

    GraphQL Errors

    A GraphQL response can contain data, errors or both.

    {
      "data": {
        "customer": {
          "id": "42",
          "name": "Example Customer",
          "email": null
        }
      },
      "errors": [
        {
          "message": "Email access is denied.",
          "path": [
            "customer",
            "email"
          ],
          "extensions": {
            "code": "FORBIDDEN",
            "traceId": "trace-8f21"
          }
        }
      ]
    }

    Partial data can be useful, but the client must inspect the errors collection instead of assuming that the presence of data means complete success.

    Safe Error Contract

    A production error should provide:

    • A safe human-readable message
    • A stable machine-readable code
    • The affected response path where applicable
    • A correlation or trace identifier
    • Structured user-input errors where required

    It should not expose:

    • Stack traces
    • SQL statements
    • Database credentials
    • Internal file paths
    • Service secrets
    • Private infrastructure names

    HTTP Status and GraphQL Errors

    HTTP and GraphQL errors represent different layers.

    Layer Examples
    HTTP transport Unsupported method, invalid media type, authentication failure or service unavailable
    GraphQL request Syntax error, validation error or unsupported field
    GraphQL execution Resolver failure, authorization rejection or downstream error
    Business operation Validation conflict, insufficient inventory or invalid lifecycle transition

    The API contract should define how HTTP status codes and GraphQL error extensions are used consistently.

    Caching

    GraphQL responses can be cached, but client-defined operations make caching different from conventional resource-specific REST caching.

    Common approaches include:

    • Normalized client-side object caching
    • Request-level caching
    • Persisted-operation caching
    • Resolver-level caching
    • Data-loader request caching
    • Application and database caching

    A cache key must account for all information affecting the result, including identity, tenant, variables, locale and authorization context.

    Unsafe shared cache
    Cache key:
    
    Operation name only
    
    
    Problem:
    
    Two users execute the same operation,
    but are authorized for different data.
    Context-aware cache design
    Cache policy considers:
    
    - Operation identity
    - Variables
    - Authenticated identity
    - Tenant
    - Authorization scope
    - Locale
    - Data version
    - Expiration policy

    Persisted Operations

    A persisted-operation design stores approved GraphQL documents on the server and allows clients to reference them using stable identifiers.

    Client sends:
    
    Persisted operation identifier
    +
    Variables
    
    
    Server:
    
    Looks up approved operation
    Validates variables
    Executes stored document

    Persisted operations can support:

    • Smaller request payloads
    • Operation allowlists
    • Precomputed complexity
    • Stable operation analytics
    • Controlled production API surface

    Persisted operations do not replace authorization, input validation or rate limiting.

    Schema Introspection

    Introspection allows authorized tooling and clients to inspect the GraphQL type system.

    Introspection supports:

    • API exploration
    • Documentation generation
    • Client code generation
    • Editor completion
    • Schema validation

    Production introspection exposure should follow the API's information- disclosure and operational policy. Disabling introspection alone does not secure an otherwise vulnerable GraphQL API.

    Schema Evolution

    GraphQL schemas commonly evolve through additive changes and deprecation.

    Additive Change

    type Customer {
      id: ID!
      name: String!
      email: String
      preferredLanguage: String
    }

    Deprecating a Field

    type Customer {
      id: ID!
    
      fullName: String
        @deprecated(
          reason: "Use displayName."
        )
    
      displayName: String!
    }

    A deprecation process should include:

    • A migration reason
    • A supported replacement
    • Usage monitoring
    • Client communication
    • A documented removal policy

    GraphQL Security Controls

    A production GraphQL service should consider:

    • Authentication before protected execution
    • Object-level authorization
    • Field-level authorization
    • Tenant isolation
    • Input validation
    • Operation depth limits
    • Complexity or cost limits
    • Alias and batch limits
    • Pagination limits
    • Request-size limits
    • Rate limits
    • Resolver deadlines
    • Safe error formatting
    • Sensitive-data redaction

    Important GraphQL Metrics

    Metric What It Helps Explain
    Operation rate Overall query, mutation and subscription workload
    Latency by operation name Slow client operations
    Resolver latency Expensive fields and downstream dependencies
    Resolver count per request Operation breadth and execution cost
    Database queries per operation N+1 query behaviour
    Operation complexity Requested computational cost
    Validation failure rate Unsupported or malformed client operations
    Authorization rejection rate Denied object and field access
    Error rate by path Fields producing execution failures
    Payload size Request and response transfer cost
    Batching efficiency Reduction in repeated data-source operations
    Active subscriptions Persistent event-stream demand

    Test a GraphQL API with curl

    Send a Query

    curl -i \
      -X POST \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json' \
      --data '{
        "operationName": "GetCustomer",
        "query": "query GetCustomer($id: ID!) { customer(id: $id) { id name status } }",
        "variables": {
          "id": "42"
        }
      }' \
      https://api.example.com/graphql

    Send a Mutation

    curl -i \
      -X POST \
      -H 'Content-Type: application/json' \
      -H 'Authorization: Bearer ACCESS_TOKEN' \
      --data '{
        "operationName": "CreateCustomer",
        "query": "mutation CreateCustomer($input: CreateCustomerInput!) { createCustomer(input: $input) { customer { id name status } errors { field code message } } }",
        "variables": {
          "input": {
            "name": "Example Customer",
            "email": "customer@example.com",
            "idempotencyKey": "customer-create-1042"
          }
        }
      }' \
      https://api.example.com/graphql

    Replace example endpoints and credentials with approved test values. Do not place production credentials directly in shell history.

    Troubleshooting Workflow

    1. Confirm the endpoint and HTTP method.
    2. Confirm the content type and request JSON.
    3. Confirm the operation name and variables.
    4. Check syntax and schema validation errors.
    5. Verify authentication.
    6. Verify object-level and field-level authorization.
    7. Inspect the returned data and errors collections.
    8. Use the trace identifier to inspect resolver telemetry.
    9. Count database and downstream calls.
    10. Check N+1 batching behaviour.
    11. Inspect operation depth and complexity.
    12. Compare runtime behaviour with the current schema.

    Common GraphQL Mistakes

    1

    Exposing Database Tables Directly

    Design the schema around stable domain concepts rather than mirroring every persistence detail.

    2

    Allowing Unlimited Query Depth

    Deeply nested operations can create excessive resolver and data-source work.

    3

    Returning Unlimited Lists

    Every collection field should enforce bounded pagination.

    4

    Ignoring the N+1 Query Problem

    Relationship resolvers can generate one data-source query per parent object unless requests are batched.

    5

    Authorizing Only the Root Query

    Nested objects and sensitive fields can require separate authorization decisions.

    6

    Marking Unreliable Fields as Non-null

    Failure of a non-null field can propagate null through a larger portion of the response.

    7

    Using Mutations without Idempotency

    Retried creation or payment mutations can create duplicate business effects.

    8

    Returning Internal Exceptions

    Stack traces, SQL errors and infrastructure details should remain in protected server telemetry.

    9

    Using Operation Depth as the Only Cost Control

    A shallow query can still request very large lists or expensive resolver fields.

    10

    Removing Fields without Deprecation

    Existing clients can continue selecting a field until they complete a controlled migration.

    11

    Caching without Authorization Context

    A response cached for one user or tenant must not be returned to another unauthorized caller.

    12

    Assuming One Endpoint Means Simple Operations

    One GraphQL endpoint can expose many differently shaped operations with very different execution costs.

    Recommended Test Cases

    Test Expected Evidence
    Valid query Only the selected fields are returned
    Unknown field The operation fails schema validation before resolver execution
    Invalid variable The request produces a structured input error
    Unauthenticated query Protected data is not resolved
    Unauthorized object The caller cannot retrieve another tenant's object
    Unauthorized field The protected field is rejected or omitted according to contract
    Deep operation The operation is rejected by the configured cost policy
    Oversized page request The server enforces the maximum page size
    N+1 test Relationship lookups are batched within the request
    Duplicate mutation retry The idempotency contract prevents duplicate effects
    Partial resolver failure The data and errors response follows the defined nullability contract
    Deprecated field usage Usage is observable while the supported replacement remains available

    GraphQL Best Practices

    Recommended Practices

    • Design the schema around stable domain concepts.
    • Use clear and consistent type and field names.
    • Use named operations in production clients.
    • Pass dynamic values through variables.
    • Use input objects for structured mutations.
    • Return useful mutation payloads and structured user errors.
    • Apply idempotency protection to safely retryable mutations.
    • Paginate every potentially large collection.
    • Define stable ordering for cursor pagination.
    • Batch relationship lookups to prevent N+1 queries.
    • Authenticate before protected execution.
    • Authorize every object, relationship and sensitive field.
    • Apply depth, complexity, alias and page-size limits.
    • Apply finite resolver and operation deadlines.
    • Use safe external error messages and protected diagnostic logs.
    • Build cache keys with identity and authorization context.
    • Prefer additive schema evolution.
    • Deprecate fields before removal.
    • Monitor operation names, resolver latency and data-source calls.
    • Use persisted operations when an allowlisted production model is appropriate.

    Practice Exercise

    Design a GraphQL API for customer and order management and compare it with the earlier REST and gRPC contracts.

    Requirements

    1. Define Customer and Order object types.
    2. Define enum types for lifecycle statuses.
    3. Define customer and order lookup queries.
    4. Add cursor-based customer and order pagination.
    5. Create a customer mutation using an input object.
    6. Create an idempotent order mutation.
    7. Return structured mutation errors.
    8. Add an order-status subscription.
    9. Authenticate protected operations.
    10. Authorize every customer and order lookup.
    11. Protect sensitive customer fields.
    12. Batch customer-to-order resolution.
    13. Apply maximum page-size and operation-cost limits.
    14. Add resolver tracing and database-query metrics.
    15. Add schema compatibility tests.

    Suggested Contract

    type Customer {
      id: ID!
      name: String!
      email: String
      status: CustomerStatus!
      orders(
        first: Int = 20
        after: String
      ): OrderConnection!
    }
    
    type Order {
      id: ID!
      status: OrderStatus!
      total: Float!
      version: Int!
      customer: Customer!
    }
    
    type OrderConnection {
      nodes: [Order!]!
      pageInfo: PageInfo!
    }
    
    type PageInfo {
      endCursor: String
      hasNextPage: Boolean!
    }
    
    input CreateOrderInput {
      idempotencyKey: String!
      customerId: ID!
      lines: [CreateOrderLineInput!]!
    }
    
    input CreateOrderLineInput {
      productId: ID!
      quantity: Int!
    }
    
    type CreateOrderPayload {
      order: Order
      errors: [UserError!]!
    }
    
    type UserError {
      field: String
      code: String!
      message: String!
    }
    
    type Query {
      customer(id: ID!): Customer
      order(id: ID!): Order
    }
    
    type Mutation {
      createOrder(
        input: CreateOrderInput!
      ): CreateOrderPayload!
    }
    
    type Subscription {
      orderUpdated(
        orderId: ID!
      ): OrderUpdate!
    }

    Frequently Asked Questions

    1

    What is GraphQL?

    GraphQL is a query language and execution model for APIs that validates client operations against a typed schema.

    2

    What is a GraphQL schema?

    A schema describes the types, fields, arguments, relationships and root operations available from a GraphQL service.

    3

    What is a GraphQL query?

    A query is a read operation containing a selection set of fields the client wants returned.

    4

    What is a mutation?

    A mutation is an operation whose top-level fields can change server-side state.

    5

    What is a resolver?

    A resolver is server-side logic responsible for producing the value of a GraphQL field.

    6

    What is the N+1 query problem?

    It occurs when one parent query is followed by one additional data-source query for every parent object.

    7

    Why should GraphQL lists be paginated?

    Pagination bounds database, resolver, memory, serialization, network and client-processing costs.

    8

    Can GraphQL return data and errors together?

    Yes. A response can contain partial data together with errors associated with selected response paths.

    9

    Does GraphQL automatically prevent unauthorized field access?

    No. The service must implement object-level, relationship-level and field-level authorization.

    10

    Does GraphQL automatically solve the N+1 problem?

    No. Resolver implementations need batching, joins, caching or another efficient data-access strategy.

    11

    Should every mutation be retried?

    No. A mutation should be retried only when its contract provides safe idempotency or another recovery mechanism.

    12

    What comes after GraphQL?

    The next topic is resource modelling, followed by pagination and versioning.

    Key Takeaway

    GraphQL exposes a typed graph of fields through a schema and allows clients to select the permitted data required for each operation. Queries read data, mutations perform controlled state changes and subscriptions deliver event-driven results. Production GraphQL services must prevent N+1 queries, paginate collections, enforce operation-cost limits, authenticate callers, authorize every object and sensitive field, protect mutations with idempotency where required and evolve the schema through additive changes and controlled deprecation.