TechLead
Lesson 3 of 20
5 min read
GraphQL

Queries and Mutations

Learn how to read data with queries and write data with mutations, including arguments, aliases, and operation names

Understanding Queries

Queries are the primary way to fetch data in GraphQL. Every GraphQL schema has a root Query type that defines all the read operations available to clients. Unlike REST where you hit different endpoints for different resources, GraphQL queries allow you to fetch multiple related resources in a single request.

Basic Queries

# Simple query — fetch all users
query GetUsers {
  users {
    id
    name
    email
  }
}

# Query with arguments — fetch a specific user
query GetUser {
  user(id: "42") {
    id
    name
    email
    role
    posts {
      id
      title
      publishedAt
    }
  }
}

# Nested queries — fetch deeply related data
query GetPostWithDetails {
  post(id: "1") {
    title
    body
    author {
      name
      avatarUrl
    }
    comments {
      text
      author {
        name
      }
      createdAt
    }
    tags {
      name
      slug
    }
  }
}

Query Arguments

Arguments let you filter, sort, and paginate data. They can be defined on any field in the schema, not just root query fields:

# Schema with various argument types
type Query {
  # Required argument
  user(id: ID!): User

  # Optional arguments with defaults
  posts(
    limit: Int = 20
    offset: Int = 0
    status: PostStatus = PUBLISHED
    sortBy: SortField = CREATED_AT
    sortOrder: SortOrder = DESC
  ): [Post!]!

  # Search with complex filter
  search(
    query: String!
    types: [SearchType!] = [POST, USER]
    limit: Int = 10
  ): [SearchResult!]!
}

# Using arguments in queries
query FilteredPosts {
  posts(limit: 5, status: DRAFT, sortBy: UPDATED_AT) {
    id
    title
    status
    updatedAt
  }
}

Aliases

Aliases let you rename the result of a field to anything you want. They are essential when you need to query the same field with different arguments in a single request:

# Without aliases, this would cause a conflict
query DashboardData {
  recentPosts: posts(limit: 5, sortBy: CREATED_AT) {
    id
    title
    createdAt
  }
  popularPosts: posts(limit: 5, sortBy: LIKES_COUNT) {
    id
    title
    likesCount
  }
  admin: user(id: "1") {
    name
    email
  }
  currentUser: user(id: "42") {
    name
    email
    role
  }
}

The response uses the alias names as keys:

{
  "data": {
    "recentPosts": [{ "id": "10", "title": "...", "createdAt": "..." }],
    "popularPosts": [{ "id": "5", "title": "...", "likesCount": 142 }],
    "admin": { "name": "Admin User", "email": "admin@example.com" },
    "currentUser": { "name": "Alice", "email": "alice@example.com", "role": "EDITOR" }
  }
}

Understanding Mutations

Mutations are used to create, update, and delete data. While queries can be executed in parallel, mutations are executed sequentially — one after another — to prevent race conditions:

# Schema mutation definitions
type Mutation {
  createPost(input: CreatePostInput!): CreatePostPayload!
  updatePost(id: ID!, input: UpdatePostInput!): UpdatePostPayload!
  deletePost(id: ID!): DeletePostPayload!
  likePost(id: ID!): LikePostPayload!
  addComment(postId: ID!, input: AddCommentInput!): AddCommentPayload!
}

input CreatePostInput {
  title: String!
  body: String!
  tags: [String!]
  isPublished: Boolean = false
}

input UpdatePostInput {
  title: String
  body: String
  tags: [String!]
  isPublished: Boolean
}

# Payload types — always return the mutated object
type CreatePostPayload {
  post: Post!
}

type UpdatePostPayload {
  post: Post!
}

type DeletePostPayload {
  success: Boolean!
  deletedId: ID!
}

Executing Mutations

# Create a new post
mutation CreateNewPost {
  createPost(input: {
    title: "Getting Started with GraphQL"
    body: "GraphQL is a query language for APIs..."
    tags: ["graphql", "api", "tutorial"]
    isPublished: true
  }) {
    post {
      id
      title
      tags { name }
      publishedAt
      author { name }
    }
  }
}

# Update an existing post
mutation UpdateExistingPost {
  updatePost(id: "1", input: {
    title: "Updated: Getting Started with GraphQL"
    isPublished: false
  }) {
    post {
      id
      title
      isPublished
      updatedAt
    }
  }
}

# Multiple mutations execute sequentially
mutation BatchOperations {
  likePost(id: "1") {
    post { likesCount }
  }
  addComment(postId: "1", input: { text: "Great article!" }) {
    comment {
      id
      text
      author { name }
    }
  }
}

Mutation Best Practices

  • Use Input types: Wrap mutation arguments in dedicated input types for clarity and reusability
  • Return Payload types: Always return a payload type that includes the mutated object so the client can update its cache
  • Verb-noun naming: Name mutations with clear verbs like createPost, updateUser, deleteComment
  • Idempotent when possible: Design mutations to produce the same result if called multiple times

Variables

In production applications, you never hardcode arguments into query strings. Instead, you use variables to pass dynamic values. Variables are declared in the operation definition and passed as a separate JSON object:

# Query with variables
query GetUser($userId: ID!) {
  user(id: $userId) {
    id
    name
    email
    posts(limit: 5) {
      title
    }
  }
}

# Mutation with variables
mutation CreatePost($input: CreatePostInput!) {
  createPost(input: $input) {
    post {
      id
      title
      publishedAt
    }
  }
}
// Using variables in Apollo Client (React)
import { useQuery, useMutation, gql } from '@apollo/client';

const GET_USER = gql`
  query GetUser($userId: ID!) {
    user(id: $userId) {
      id
      name
      email
    }
  }
`;

const CREATE_POST = gql`
  mutation CreatePost($input: CreatePostInput!) {
    createPost(input: $input) {
      post {
        id
        title
      }
    }
  }
`;

function UserProfile({ userId }: { userId: string }) {
  const { data, loading, error } = useQuery(GET_USER, {
    variables: { userId },
  });

  const [createPost] = useMutation(CREATE_POST);

  const handleSubmit = async (title: string, body: string) => {
    await createPost({
      variables: {
        input: { title, body, isPublished: true },
      },
    });
  };

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <div>
      <h1>{data.user.name}</h1>
      <p>{data.user.email}</p>
    </div>
  );
}

Operation Names

While anonymous queries work in development, named operations are required for production. They help with debugging, logging, server-side analytics, and persisted queries:

# Anonymous (avoid in production)
{
  users { id name }
}

# Named operations (recommended)
query GetAllUsers {
  users { id name }
}

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

mutation SignUp($input: SignUpInput!) {
  signUp(input: $input) { token user { id name } }
}

Key Takeaways

  • Queries read data, mutations write data — this separation makes APIs predictable
  • Aliases prevent field conflicts — essential when querying the same field multiple times
  • Variables keep queries reusable — never hardcode values into query strings
  • Mutations run sequentially — GraphQL guarantees ordering for write operations
  • Name your operations — it improves debugging, logging, and tooling support

Continue Learning