Chapter 4: Understanding GraphQL Queries

Chapter 4: Understanding GraphQL Queries

Understanding GraphQL Queries

Imagine you’re at a restaurant with a super customizable menu. Instead of ordering a bunch of separate dishes to get what you want, you can tell the chef exactly what ingredients you’d like in one perfect meal. That’s kinda what a GraphQL query does! It’s a way to ask a server for exactly the data you need in one go. Unlike older-school REST APIs, where you might need to ping multiple spots to piece together info, GraphQL lets you shape your request like a wishlist, and the server sends back data that fits it perfectly. Cool, right?

Basic Structure

A GraphQL query looks like this:

graphql

query {
  user(id: "123") {
    name
    email
    posts {
      title
      content
    }
  }
}
  • query: The operation type (optional keyword for readability; if omitted, it’s still a query by default).
  • user: The root field or resource you’re querying.
  • id: “123”: An argument to specify which user to fetch.
  • name, email, posts: The fields you want returned. Nested fields like posts { title, content } allow you to dig into related data.

The response would be JSON matching this structure, like:

json

{
  "data": {
    "user": {
      "name": "Alicee",
      "email": "alicee@example.com",
      "posts": [
        {
          "title": "My First Post",
          "content": "Hello, world!"
        }
      ]
    }
  }
}

Key Features of GraphQL Queries

  1. Field Selection: You only get what you ask for—no over-fetching or under-fetching.
  2. Arguments: Pass parameters (e.g., id: “123”) to filter or customize results.
  3. Nesting: Retrieve related data (e.g., a user’s posts) in one go.
  4. Aliases: Rename fields in the response for clarity (e.g., userName: name).
  5. Fragments: Reuse common field selections across queries.

Example with Aliases and Fragments

graphql

query {
  alice: user(id: "123") {
    ...userFields
  }
  bob: user(id: "456") {
    ...userFields
  }
}

fragment userFields on User {
  name
  email
}

Response:

json

{
  "data": {
    "alice": {
      "name": "Alicee",
      "email": "alicee@example.com"
    },
    "bob": {
      "name": "Bob",
      "email": "bob@example.com"
    }
  }
}

Variables

For dynamic queries, use variables:

graphql

query GetUser($userId: ID!) {
  user(id: $userId) {
    name
    email
  }
}

Variable input:

json

{
  "userId": "123"
}

Why It Matters

GraphQL queries empower clients (like web or mobile apps) to be precise, reducing bandwidth and simplifying frontend logic. They’re introspectable too—tools can explore the schema to see what’s possible.

GraphQL syntax

GraphQL syntax is the backbone of how you create queries, mutations, and subscriptions to work with a GraphQL API. It’s built to be clear and straightforward, letting you define exactly what data you need in a structured format. Here’s a breakdown of the core components of GraphQL syntax, with examples to make it click.

Core Components of GraphQL Syntax

1. Operations

GraphQL has three main operation types:

  • Query: Fetch data (read-only).
  • Mutation: Modify data (create, update, delete).
  • Subscription: Listen for real-time updates.

Each operation starts with its type (optional for queries) and can have a name for clarity.

Example:

graphql

query GetUser {
  user(id: "123") {
    name
  }
}

2. Fields

Fields are the pieces of data you request or modify. They can be scalar (e.g., String, Int) or object types with nested fields.

Example:

graphql

{
  user {
    name        # Scalar field
    address {   # Object field
      street
      city
    }
  }
}

3. Arguments

Fields can take arguments to filter or customize the response. Arguments are key-value pairs in parentheses.

Example:

graphql

query {
  user(id: "123", active: true) {
    name
    email
  }
}

4. Variables

Variables make queries dynamic by allowing values to be passed separately. Define them with a $ prefix and a type, then use them in the query.

Syntax:

graphql

query GetUser($id: ID!, $limit: Int) {
  user(id: $id) {
    name
    posts(limit: $limit) {
      title
    }
  }
}

Variable input (JSON):

json

{
  "id": "123",
  "limit": 5
}
  • $id: ID!: ! means it’s required (non-nullable).
  • $limit: Int: Optional argument (nullable by default).

5. Aliases

Aliases let you rename fields in the response to avoid conflicts or improve clarity.

Example:

graphql

{
  user1: user(id: "123") {
    name
  }
  user2: user(id: "456") {
    name
  }
}

Response:

json

{
  "data": {
    "user1": { "name": "Alicee" },
    "user2": { "name": "Bob" }
  }
}

6. Fragments

Fragments allow you to reuse a set of fields across multiple parts of a query.

Syntax:

graphql

fragment UserInfo on User {
  name
  email
}

query {
  user(id: "123") {
    ...UserInfo
    age
  }
}

Response includes name, email, and age.

7. Directives

Directives (@) modify the execution of a query. Common ones are:

  • @include(if: Boolean): Include a field if true.
  • @skip(if: Boolean): Skip a field if true.

Example:

graphql

query GetUser($withEmail: Boolean!) {
  user(id: "123") {
    name
    email @include(if: $withEmail)
  }
}

If withEmail: true, email is included; if false, it’s skipped.

8. Mutations

Mutations follow a similar syntax to queries but are used to change data.

Example:

graphql

mutation CreateUser($input: UserInput!) {
  createUser(input: $input) {
    id
    name
  }
}

Input:

json

{
  "input": {
    "name": "Alicee",
    "email": "alicee@example.com"
  }
}

9. Subscriptions

Subscriptions use the same syntax but enable real-time updates via WebSockets.

Example:

graphql

subscription {
  messageAdded(channelId: "1") {
    id
    content
  }
}

10. Inline Fragments

For union or interface types, inline fragments (… on Type) specify fields for a specific type.

Example:

graphql

{
  search(text: "test") {
    ... on User {
      name
    }
    ... on Post {
      title
    }
  }
}

Syntax Rules

  • Curly braces {}: Enclose field selections.
  • Parentheses (): Wrap arguments or variables.
  • Colon :: Separates alias names or argument names from values.
  • Exclamation mark !: Denotes non-nullable types.
  • Comments #: Single-line comments are supported.

Full Example

Here’s a comprehensive query combining several features:

graphql

query FetchUserData($userId: ID!, $showPosts: Boolean! = true) {
  user(id: $userId) {
    name
    email @skip(if: $showPosts)
    posts(limit: 2) @include(if: $showPosts) {
      title
      author {
        ...UserInfo
      }
    }
  }
}

fragment UserInfo on User {
  name
  email
}

Input:

json

{
  "userId": "123",
  "showPosts": true
}

This fetches a user’s name and, if showPosts is true, their recent posts with author details.

Filtering, ordering, pagination

In GraphQL, filtering, ordering, and pagination are common patterns for controlling the data returned by a query. Unlike REST, where these might be handled with query parameters or multiple endpoints, GraphQL relies on arguments and schema design to implement them. Here’s how each works in GraphQL syntax, with examples.


1. Filtering

Filtering narrows down the dataset based on specific conditions. It’s typically implemented by passing arguments to fields that the server uses to filter results.

Syntax

You define filter criteria as arguments, often using scalar values (e.g., String, Int) or custom input types.

Example

graphql

query {
  users(filter: { role: "ADMIN", isActive: true }) {
    id
    name
    role
  }
}
  • filter: { role: “ADMIN”, isActive: true }: Only returns users who are admins and active.
  • The server’s schema defines what fields are filterable (e.g., role, isActive).

Using an Input Type

For complex filters, an input type might be used:

graphql

input UserFilter {
  role: String
  isActive: Boolean
  minAge: Int
}

query GetFilteredUsers($filter: UserFilter) {
  users(filter: $filter) {
    id
    name
  }
}

Input:

json

{
  "filter": {
    "role": "ADMIN",
    "minAge": 25
  }
}

Response

json

{
  "data": {
    "users": [
      { "id": "1", "name": "Alicee" },
      { "id": "2", "name": "Bob" }
    ]
  }
}

2. Ordering (Sorting)

Ordering specifies how results should be sorted (e.g., ascending or descending). This is often done with arguments like orderBy or sort, which might take an enum or string value.

Syntax

Pass an argument to define the field to sort by and the direction.

Example

graphql

query {
  users(orderBy: { field: "name", direction: "ASC" }) {
    id
    name
  }
}
  • field: “name”: Sort by the name field.
  • direction: “ASC”: Ascending order (could be “DESC” for descending).

Using an Enum

A schema might define an enum for sorting:

graphql

enum SortDirection {
  ASC
  DESC
}

query GetSortedUsers($sort: SortDirection) {
  users(orderBy: { field: "createdAt", direction: $sort }) {
    id
    createdAt
  }
}

Input:

json

{
  "sort": "DESC"
}

Response

Users sorted by createdAt in descending order:

json

{
  "data": {
    "users": [
      { "id": "3", "createdAt": "2025-05-01" },
      { "id": "1", "createdAt": "2025-04-15" }
    ]
  }
}

3. Pagination

Pagination limits the number of results returned and allows fetching them in chunks. Common approaches include:

  • Offset-based: Use offset and limit.
  • Cursor-based: Use first, after, last, before with a cursor (opaque string).
  • Page-based: Use page and perPage.

Offset-Based Pagination

Simple but less efficient for large datasets.

graphql

query {
  users(offset: 10, limit: 5) {
    id
    name
  }
}
  • offset: 10: Skip the first 10 results.
  • limit: 5: Return the next 5.

Cursor-Based Pagination (Relay Style)

More efficient and common in GraphQL (inspired by Relay). Uses edges and nodes.

graphql

query {
  users(first: 2, after: "cursor123") {
    edges {
      node {
        id
        name
      }
      cursor
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}
  • first: 2: Get the first 2 items after the cursor.
  • after: “cursor123”: Start after this cursor (from a previous query).
  • edges: Array of results with node (the data) and cursor (pagination marker).
  • pageInfo: Metadata for navigation.

Response

json

{
  "data": {
    "users": {
      "edges": [
        { "node": { "id": "1", "name": "Alice" }, "cursor": "cursor456" },
        { "node": { "id": "2", "name": "Bob" }, "cursor": "cursor789" }
      ],
      "pageInfo": {
        "hasNextPage": true,
        "endCursor": "cursor789"
      }
    }
  }
}
  • Use endCursor in the next after argument to fetch the next page.

Combined Example

Here’s filtering, ordering, and pagination together:

graphql

query GetUsers($filter: UserFilter, $sort: SortDirection, $first: Int, $after: String) {
  users(filter: $filter, orderBy: { field: "name", direction: $sort }, first: $first, after: $after) {
    edges {
      node {
        id
        name
      }
      cursor
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

Input:

json

{
  "filter": { "role": "USER" },
  "sort": "ASC",
  "first": 2,
  "after": "cursor123"
}

Response

json

{
  "data": {
    "users": {
      "edges": [
        { "node": { "id": "4", "name": "Charlie" }, "cursor": "cursor456" },
        { "node": { "id": "5", "name": "Dana" }, "cursor": "cursor789" }
      ],
      "pageInfo": {
        "hasNextPage": true,
        "endCursor": "cursor789"
      }
    }
  }
}

Notes

  • Schema Dependency: The exact arguments (filter, orderBy, first, etc.) depend on how the GraphQL server defines its schema. Check the API’s documentation or introspection.
  • Performance: Cursor-based pagination is preferred for scalability over offset-based, as it avoids re-counting large datasets.
  • Flexibility: GraphQL doesn’t enforce a standard; implementations vary (e.g., Relay-style vs. custom).

Nested queries and entity relationships

Hey there! In GraphQL, nested queries and entity relationships are super cool because they let you grab all the related data you need in just one go. Unlike REST, where you might have to hit multiple endpoints to piece things together, GraphQL allows you to dive into relationships by nesting fields in your query. It’s all about that hierarchical vibe in GraphQL schemas, where stuff like users, posts, and comments are connected through fields.

Let me break down how nested queries work, explain how they handle entity relationships, and toss in some examples to make it clear.


Nested Queries

A nested query is when you request fields within fields, diving deeper into related data. The structure of the query mirrors the relationships defined in the GraphQL schema.

How It Works

  • The schema defines types (e.g., User, Post) and their fields.
  • Fields can return scalar types (e.g., String, Int) or object types (e.g., Post, [Comment]).
  • Nesting occurs when you query an object field and then request its subfields.

Example

Suppose you have a schema with users who write posts, and posts have comments:

graphql

type User {
  id: ID!
  name: String!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  comments: [Comment!]!
}

type Comment {
  id: ID!
  text: String!
  author: User!
}

A nested query to fetch a user, their posts, and comments might look like this:

graphql

query {
  user(id: "123") {
    name
    posts {
      title
      comments {
        text
        author {
          name
        }
      }
    }
  }
}

Response

json

{
  "data": {
    "user": {
      "name": "Alice",
      "posts": [
        {
          "title": "My First Post",
          "comments": [
            {
              "text": "Great post!",
              "author": { "name": "Bob" }
            },
            {
              "text": "Thanks for sharing!",
              "author": { "name": "Charlie" }
            }
          ]
        }
      ]
    }
  }
}
  • Nesting: user → posts → comments → author.
  • Each level corresponds to a relationship in the schema.

Entity Relationships

GraphQL models relationships between entities using fields that return object types or lists. Common relationship types include:

  1. One-to-One: A single entity relates to another (e.g., Post.author: User).
  2. One-to-Many: An entity relates to multiple entities (e.g., User.posts: [Post!]).
  3. Many-to-Many: Entities connect through an intermediary (e.g., users and groups via memberships, often simplified in GraphQL).

Defining Relationships

Relationships are defined in the schema by the type system:

  • User.posts: [Post!]: A user has many posts (non-nullable list).
  • Post.author: User!: A post has one author (non-nullable).
  • Comment.author: User!: A comment has one author.

Querying Relationships

You traverse these relationships by nesting fields. Arguments can filter or refine the related data.

One-to-Many Example

Fetch a user and their recent posts:

graphql

query {
  user(id: "123") {
    name
    posts(limit: 2, orderBy: { field: "createdAt", direction: "DESC" }) {
      title
      createdAt
    }
  }
}

Response:

json

{
  "data": {
    "user": {
      "name": "Alice",
      "posts": [
        { "title": "Latest Post", "createdAt": "2025-04-09" },
        { "title": "Older Post", "createdAt": "2025-04-01" }
      ]
    }
  }
}

Many-to-One Example

Fetch a post and its author:

graphql

query {
  post(id: "456") {
    title
    author {
      name
      email
    }
  }
}

Response:

json

{
  "data": {
    "post": {
      "title": "My First Post",
      "author": {
        "name": "Alice",
        "email": "alice@example.com"
      }
    }
  }
}

Bidirectional Relationships

You can traverse relationships in both directions. For example, from a comment to its post and back to the post’s author:

graphql

query {
  comment(id: "789") {
    text
    post {
      title
      author {
        name
      }
    }
  }
}

Response:

json

{
  "data": {
    "comment": {
      "text": "Great post!",
      "post": {
        "title": "My First Post",
        "author": { "name": "Alice" }
      }
    }
  }
}

Advanced Nesting with Filtering and Pagination

You can apply filtering, ordering, and pagination to nested fields.

Example

Fetch a user, their posts (filtered and paginated), and each post’s comments:

graphql

query {
  user(id: "123") {
    name
    posts(first: 2, after: "cursorXYZ", filter: { published: true }) {
      edges {
        node {
          title
          comments(limit: 3, orderBy: { field: "createdAt", direction: "ASC" }) {
            text
            author {
              name
            }
          }
        }
        cursor
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
}

Response

json

{
  "data": {
    "user": {
      "name": "Alice",
      "posts": {
        "edges": [
          {
            "node": {
              "title": "Post 1",
              "comments": [
                { "text": "First!", "author": { "name": "Bob" } },
                { "text": "Nice!", "author": { "name": "Charlie" } }
              ]
            },
            "cursor": "cursorABC"
          },
          {
            "node": {
              "title": "Post 2",
              "comments": [
                { "text": "Cool!", "author": { "name": "Dana" } }
              ]
            },
            "cursor": "cursorDEF"
          }
        ],
        "pageInfo": {
          "hasNextPage": true,
          "endCursor": "cursorDEF"
        }
      }
    }
  }
}

Key Points

  • Schema-Driven: Nesting depends on how relationships are defined in the schema (e.g., posts: [Post!]).
  • Flexibility: You control the depth and shape of the response.
  • Performance: Deep nesting can strain servers (e.g., N+1 query problem), so resolvers often optimize with batching or DataLoader.
  • Circular References: Possible (e.g., Post.author.posts.author), but depth is usually limited by the client or server.

Example: Query top ERC-20 token holders

Hey! If you want to query the top holders of an ERC-20 token using GraphQL, you’d need a setup where the token holder data, like wallet addresses and their balances, is available in a schema. The thing is, standard ERC-20 contracts (like the ones from OpenZeppelin) don’t keep a handy list of holders on-chain—they just store balances in a mapping (think address-to-amount). That makes it tricky to query directly without some extra help. Usually, services like The Graph, Etherscan, or custom tools step in by indexing Transfer events to keep track of who’s holding what.

Since you’re asking for a GraphQL query example, I’ll imagine we’re working with a schema from a provider like Bitquery or Chainbase that’s already indexed the ERC-20 holder data. I’ll show you what that query could look like and explain how it works.


Example GraphQL Query

Let’s query the top 10 holders of an ERC-20 token, such as USDT (Tether) on Ethereum, sorted by balance in descending order.

graphql

query TopTokenHolders {
  tokenHolders(
    tokenAddress: "0xdac17f958d2ee523a2206206994597c18d831ec7"  # USDT contract address
    network: "ethereum"
    limit: 10
    orderBy: { field: "balance", direction: "DESC" }
  ) {
    holderAddress
    balance
    token {
      symbol
      decimals
    }
  }
}

Hypothetical Response

json

{
  "data": {
    "tokenHolders": [
      {
        "holderAddress": "0x3f5ce5fbfe3e9af3971dd833d26ba9b5c936f0be",
        "balance": "5000000000000",
        "token": {
          "symbol": "USDT",
          "decimals": 6
        }
      },
      {
        "holderAddress": "0x47ac0fb4f2d84898e4d9e7b4dab3c24507a6d503",
        "balance": "3000000000000",
        "token": {
          "symbol": "USDT",
          "decimals": 6
        }
      }
      // ... 8 more holders
    ]
  }
}
  • Balances are in raw units (e.g., 5000000000000 = 5,000,000 USDT, adjusted for 6 decimals).

Breaking Down the Query

  1. tokenHolders Field:
    • The root field representing the list of token holders.
    • Arguments like tokenAddress specify the ERC-20 contract (here, USDT).
  2. Arguments:
    • tokenAddress: The ERC-20 contract address (e.g., USDT’s 0xdac17f…).
    • network: Specifies the blockchain (e.g., “ethereum”).
    • limit: 10: Restricts results to the top 10 holders.
    • orderBy: { field: “balance”, direction: “DESC” }: Sorts by balance, highest to lowest.
  3. Selected Fields:
    • holderAddress: The wallet address of the token holder.
    • balance: The raw token balance (unadjusted for decimals).
    • token { symbol, decimals }: Metadata about the token for context.

Real-World Context

To execute this query, you’d need:

  • A Subgraph: A custom GraphQL endpoint (e.g., via The Graph) indexing Transfer events for the token. The subgraph would process all transfers to compute current balances and expose them in a queryable format.
  • API Provider: Services like Bitquery, Chainbase, or GoldRush offer APIs that can return this data. For example:
    • Chainbase’s API: GET /v1/token/holders?chain_id=1&contract_address=0xdac17f…&limit=10.
    • You’d adapt the GraphQL query to their schema.

Schema Assumption

The example assumes a schema like:

graphql

type TokenHolder {
  holderAddress: String!
  balance: String!
  token: Token!
}

type Token {
  symbol: String!
  decimals: Int!
}

type Query {
  tokenHolders(
    tokenAddress: String!
    network: String!
    limit: Int
    orderBy: OrderByInput
  ): [TokenHolder!]!
}

input OrderByInput {
  field: String!
  direction: String!
}

Nested Relationships

You could extend this to include relationships, like the holder’s transactions:

graphql

query TopTokenHoldersWithTransactions {
  tokenHolders(
    tokenAddress: "0xdac17f958d2ee523a2206206994597c13d831ec7"
    network: "ethereum"
    limit: 10
    orderBy: { field: "balance", direction: "DESC" }
  ) {
    holderAddress
    balance
    token {
      symbol
    }
    transactions(first: 2) {
      txHash
      amount
      timestamp
    }
  }
}

Response

json

{
  "data": {
    "tokenHolders": [
      {
        "holderAddress": "0x3f5ce5fbfe3e9af3971dd833d26ba9b5c936f0be",
        "balance": "5000000000000",
        "token": { "symbol": "USDT" },
        "transactions": [
          { "txHash": "0xabc...", "amount": "1000000000", "timestamp": "2025-04-01" },
          { "txHash": "0xdef...", "amount": "2000000000", "timestamp": "2025-03-15" }
        ]
      }
      // ...
    ]
  }
}

Notes

  • Data Source: This query assumes an indexed datasource. Without it, you’d need to scan all Transfer events off-chain and calculate balances manually.
  • Decimals: Balances are raw; divide by 10^decimals (e.g., 10^6 for USDT) for human-readable values.
  • Scalability: For real-time top holders, pagination (first, after) might be added.

Comments

No comments yet. Why don’t you start the discussion?

    Leave a Reply

    Your email address will not be published. Required fields are marked *