92.1 GraphQL Fundamentals: Queries, Mutations, Subscriptions
Right, let’s get this straight. You’ve probably been sold GraphQL as the one true way to unshackle your frontend from the tyranny of rigid REST endpoints. And for the most part, that’s true. But it’s not magic. It’s a language, a specification, and like any language, you can use it to write poetry or to create incomprehensible, nested monstrosities that haunt your dreams. I’m here to help you write the poetry.
At its core, GraphQL is about asking for exactly what you want. No more, no less. This happens through three primary operations: you Query to read data, you Mutate to change data, and you Subscribe to be notified when data changes. It’s the holy trinity of CRUD, but with a type-safe, self-documenting twist.
The Art of the Query: Asking Nicely
Think of a query as a politely worded, highly specific request. You’re not yelling “Gimme user data!” at your API. You’re saying, “Hello, esteemed server. If it’s not too much trouble, may I please have the id, name, and email for the user with an id of 42? And while you’re at it, for that user, could you also tell me the titles of their last three blog posts? Thank you ever so much.”
This translates to:
query GetUserWithPosts {
user(id: 42) {
id
name
email
posts(limit: 3, orderBy: createdAt_DESC) {
title
}
}
}
The server, if all is well, responds with a perfect JSON mirror of your request structure:
{
"data": {
"user": {
"id": "42",
"name": "Ada Lovelace",
"email": "ada@example.com",
"posts": [
{ "title": "On the Algorithmic Mind" },
{ "title": "First Thoughts on the Analytical Engine" }
]
}
}
}
Why this rules: You got everything you needed in one predictably shaped round trip. No under-fetching (having to call /users/42 and then /users/42/posts). No over-fetching (getting a user object with 50 fields when you only needed three). The frontend is in control. The payoff in mobile performance alone is often worth the price of admission.
The pitfall: It’s tempting to create massive, deeply nested queries that join half your database. Don’t. Your backend still has to resolve all that data. A query asking for user.posts.comments.author.profile.image might trigger a “join storm” that kills your database. You, the query writer, must be aware of the data load you’re requesting. The power is yours, and with it comes responsibility. Use tools like persisted queries or query cost analysis in production to avoid these foot guns.
Mutations: Changing State with Intent
Queries are for reading. Mutations are for writing. This is a crucial distinction the GraphQL spec makes, and it’s a good one. It means you can quickly identify operations that change state, which is vital for debugging and security. By convention, queries can be executed in parallel, while mutations are executed serially on the server to avoid race conditions. Neat, huh?
A mutation looks almost identical to a query, but it starts with the mutation keyword. This is your way of shouting “Heads up, I’m about to change stuff!”
mutation CreatePostAndConnectIt {
createPost(
title: "My New Post"
content: "This is written via a GraphQL mutation. Meta."
authorId: "42"
) {
id
title
createdAt # You almost always want the new state returned
}
}
See what we did there? We’re not just mutating blindly. We’re asking for specific fields back from the newly created post. This is the best practice. Always ask for data back from a mutation. Why fire a mutation to update a user’s name and then have to issue a separate query to get the new name to display in your UI? One round trip, please.
The rough edge: The designers, in their infinite wisdom, decided mutations also use curly braces. This leads to the single most common GraphQL faux pas I see: writing a mutation with an empty selection set. mutation { createPost(...) } is invalid. You must ask for something back, even if it’s just { success } or the id. The server will yell at you, and rightly so.
Subscriptions: Your Live Data Lifeline
Here’s where we leave HTTP behind and break out the champagne (or the headache medicine). Subscriptions are GraphQL’s real-time operation. You open a long-lived connection (almost always a WebSocket) to the server, send a subscription query, and then just listen. The server pushes a new result to you every time the data you subscribed to changes.
It looks deceptively like a query:
subscription OnNewPostPublished {
postPublished {
id
title
author {
name
}
}
}
You set this up client-side, and then you just wait. When someone else triggers that createPost mutation from earlier, the server knows a new post was published. It executes the postPublished subscription resolver and pushes the data—the id, title, and author’s name—to every connected client listening to OnNewPostPublished.
Why it’s absurdly powerful: This is how you build live feeds, collaborative editors, real-time dashboards, or chat features. Your UI updates instantly as the backend state changes, without you constantly polling an endpoint every few seconds like a caveman.
The massive, glaring pitfall: Subscriptions are the trickiest part of the GraphQL ecosystem to implement well on the server. The spec is wonderfully vague on the transport layer. While everyone uses WebSockets, the protocol on top (e.g., graphql-ws vs legacy subscriptions-transport-ws) is a minefield of outdated tutorials. Furthermore, you now have to manage stateful connections, authentication over WS, and a pub/sub system (like Redis) to broadcast events between your server instances. It’s not for the faint of heart. Choose your server library (like Apollo Server) carefully and follow their subscription guide to the letter.
So there you have it. Queries, Mutations, Subscriptions. The three pillars. Use them wisely. Build fast apps. And for the love of all that is holy, don’t forget the selection set on your mutations. I will know.