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