Queries, Mutations, Subscriptions
GraphQL has three root operation types. Each maps to a different access pattern. The schema declares them; clients pick one per request.
Query — read
query GetUser($id: ID!) {
user(id: $id) {
name
email
posts(limit: 5) {
title
createdAt
}
}
}
Variables passed separately:
{"query": "...", "variables": {"id": 42}}
Queries should be side-effect-free (like HTTP GET). The server might run them in parallel; it can cache them; it can retry them. Don’t mutate state in a query.
Multiple queries in one operation:
query {
me { name }
popularPosts: posts(sort: VIEWS, limit: 10) { title }
notifications { count }
}
All three execute in parallel (typically). One round trip, three results.
Aliases
When you call the same field twice with different args:
query {
alice: user(id: 1) { name email }
bob: user(id: 2) { name email }
}
Without aliases, the result would have duplicate user keys. Aliases give them distinct names:
{
"data": {
"alice": {"name": "Alice", "email": "a@b.com"},
"bob": {"name": "Bob", "email": "b@c.com"}
}
}
Fragments — reuse query shape
fragment UserCard on User {
id
name
avatar
}
query {
current: me { ...UserCard }
team: users(role: ADMIN) { ...UserCard }
}
Define the field set once; reuse it. Frontend codegen tools (graphql-codegen, Apollo) often generate typed fragments per component.
Variables and operation name
query GetUser($id: ID!, $postLimit: Int = 5) {
user(id: $id) {
name
posts(limit: $postLimit) { title }
}
}
Variables:
- Declared in the operation signature with type annotations.
- Can have default values (
$postLimit: Int = 5). - Sent as a separate JSON object — keeps the query string stable for caching and logging.
GetUser is the operation name. Useful when a document has multiple operations:
{"query": "...two operations...", "operationName": "GetUser", "variables": {...}}
Always name operations in production code — server logs and metrics use the name to group.
Mutation — write
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
createdAt
}
}
Same shape as queries, just rooted at Mutation instead of Query. The difference: mutations execute serially, not in parallel. The spec guarantees this so that mutation { deleteUser ... createUser ... } runs deterministically.
Common patterns:
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): DeleteResult!
}
The mutation returns the affected resource (or an envelope) — clients can immediately update local state without a follow-up query.
Mutation envelopes — for richer errors
Plain mutation returning the model:
type Mutation {
createUser(input: CreateUserInput!): User!
}
Throws on failure (top-level errors array). Better pattern — return a result type:
type Mutation {
createUser(input: CreateUserInput!): CreateUserResult!
}
union CreateUserResult = CreateUserSuccess | ValidationError | EmailTaken
type CreateUserSuccess {
user: User!
}
type ValidationError {
fields: [FieldError!]!
}
type EmailTaken {
message: String!
}
Now business errors are part of the schema. Clients exhaustively handle each case:
mutation {
createUser(input: {...}) {
__typename
... on CreateUserSuccess { user { id name } }
... on ValidationError { fields { name message } }
... on EmailTaken { message }
}
}
This is the “errors as data” pattern — pioneered by Relay, increasingly common. Trade-off: more schema surface area but cleaner client error handling.
Subscription — push
subscription OnMessageAdded($roomId: ID!) {
messageAdded(roomId: $roomId) {
id
author { name }
text
createdAt
}
}
The client receives updates as they happen. Unlike queries/mutations (which return once), subscriptions stream — typically over WebSocket.
Transport
The GraphQL spec doesn’t define the transport. Two common:
| Transport | Status |
|---|---|
graphql-ws (the newer protocol over WebSocket) |
current standard |
subscriptions-transport-ws (older WebSocket protocol) |
deprecated |
| SSE-based | also possible (Apollo, GraphQL-Yoga support it) |
The WebSocket handshake includes auth (typically via Connection Init payload). Each subscription gets an ID; server pushes events tagged with that ID.
Implementation
Subscriptions need a publish-subscribe broker. Pure in-process pub/sub works for single-server demos; for production, Redis pub/sub, NATS, or Kafka.
# Strawberry example
import strawberry
import asyncio
from typing import AsyncIterator
@strawberry.type
class Subscription:
@strawberry.subscription
async def message_added(self, room_id: strawberry.ID) -> AsyncIterator[Message]:
# subscribe to a Redis channel for this room
async for message in pubsub.subscribe(f"room:{room_id}"):
yield Message(**message)
When to use subscriptions
- Real-time UI updates (chat, notifications, live dashboards, collaborative editing).
- Low-frequency events (~ <10/sec per client).
- Per-user data where the client already has session context.
When NOT:
- High-frequency streams (>100/sec) — WebSocket per client gets expensive; consider SSE or dedicated streaming protocols.
- Mostly-static data with rare updates — polling is simpler.
- Massive fan-out (one event to 100k clients) — needs careful broker design; consider WebSocket-aware load balancers.
Operation type choice
| Need | Use |
|---|---|
| read data | Query |
| mutate state | Mutation |
| stream updates | Subscription |
| trigger a side-effect (send email, kick off job) | Mutation, even if no “data” changes |
| webhook-style notify | Mutation that returns success/failure |
Persisted queries — for performance + security
Instead of sending the full query string each time:
POST /graphql
{"query": "query GetUser($id: ID!) { user(id: $id) { name email } }", "variables": {"id": 42}}
You can register the query at deploy time and reference by hash:
GET /graphql?queryId=sha256:abc123...&variables=...
Pros:
- Smaller request body.
- Cacheable (GET with stable URL).
- Security: server only accepts known queries — clients can’t run arbitrary expensive queries.
Cons:
- Build step required (registration).
- Less flexible for one-off queries.
Apollo and Relay support this; “automatic persisted queries” hash the query client-side and register on first request.
Common pitfalls
- Mutating data in a query resolver — breaks the parallel execution guarantee; intermittent bugs.
- No operation name — server logs show anonymous; can’t tell which operation is slow.
- Subscription without auth — anyone can subscribe to any channel. Auth at connection init AND per-subscription.
- Subscription channel without filtering — server pushes every event to every subscriber; clients filter client-side. Server-side filtering by subscription args.
- Unbounded list args —
users(ids: [ID!]!)with no limit; clients send 10000 IDs.
Common interview confusions
- “Subscriptions are like polling.” — opposite: server pushes when events occur. Polling is the client asking repeatedly.
- “You can mutate in a query.” — the spec allows it but doesn’t guarantee execution order. Don’t do it.
- “Mutations always succeed unless network fails.” — they can fail business rules; check the response.
Interview angle
- “Difference between query and mutation in execution?” — fields in a query may execute in parallel; mutation fields execute serially (in declared order). Spec guarantees this so multiple mutations in one request behave predictably.
- “What are fragments and why use them?” — reusable selection sets. Define
fragment UserCard on User { id name avatar }once; reuse in multiple queries. Pairs with frontend codegen to type per-component data. - “What’s a GraphQL subscription?” — long-lived operation that streams updates as events happen. Typically WebSocket transport via
graphql-ws. Needs a server-side pub/sub broker (Redis, NATS, Kafka). - “When should you use subscriptions vs polling vs webhooks?” — subscriptions for in-app real-time UI (browser, mobile). Polling for simple low-frequency cases. Webhooks for server-to-server.
- “What’s an ‘errors as data’ mutation pattern?” — instead of throwing top-level errors, the mutation returns a union of success/error result types. Clients exhaustively handle each — cleaner error UX, but more schema surface area.
- “What are persisted queries?” — query strings registered at deploy time, referenced by hash. Saves bandwidth, enables GET caching, and prevents arbitrary client queries (a security win).