Table of contents
I recently saw a post on CSS-Tricks by Sebastian Scholl that explained what’s so great about GraphQL, the hot, new(ish) way to write an API:
GraphQL allows developers to articulate what they want with a tidy query and, in response, only receive what they specified. Those queries are fully dynamic, so only a single endpoint is required. REST, on the other hand, has predefined responses and often requires applications to utilize multiple endpoints to satisfy a full data requirement.
The above is just one aspect of why GraphQL is dope, but there’s something broader here. I think a GraphQL service is clearer, easier to understand, and easier to use than your typical REST API, and the tooling is gobsmackingly awesome.
My team started using GraphQL last year. Although I was initially a skeptic, now I’m a convert.
What is GraphQL?
If you have no idea what GraphQL is, I recommend this other post on CSS-Tricks by Peggy Rayzis.
In short:
- You use the GraphQL specification to define your schema: what data is available (the special
Query
type), what the shape of that data is, and how it can change (the specialMutation
type) - You feed that schema into a backend service like Apollo Server. That service will provide a single endpoint that others can use to query for data.
- You also define resolver functions that tell the service exactly how to fetch the data when needed. More on this later!
- Clients, for example a web application, can send queries to your GraphQL service and get back data.
What’s so great about it?
For the following, let’s say you’re running a social network. You’ve got users. And you’ve set up a GraphQL service to get information about them.
Your User
model lets you see a user’s name, profile picture, and a list of other users that they’re following. That type in GraphQL would look like this:
type User {
id: String
name: String
profileImageUrl: String
email: String
following: [User]
followers: [User]
}
And you can query for users by their ID. In your GraphQL service schema, that looks like this:
type Query {
user(id: String!): User
}
Request and receive only what you need
The is the benefit that everyone talks about first. For good reason! REST APIs tend to include way more information than you probably immediately need.
If your client at a particular point only needs a user’s name and profile picture, you can just ask for that:
{
user(id: "leereamsnyder") {
name
profileImageUrl
}
}
And the GraphQL service will respond with that, and only that:
{
"data": {
"user": {
"name": "Lee Reamsnyder",
"profileImageUrl": "http://example.com/image.png"
}
}
}
If you need their list of followers, but only their names, same deal. Just add that to the query:
{
user(id: "leereamsnyder") {
name
profileImageUrl
followers {
name
}
}
}
And you’ll receive:
{
"data": {
"user": {
"name": "Lee Reamsnyder",
"profileImageUrl": "http://example.com/image.png"
"followers": [
{"name": "Puff the Magic Dragon"},
{"name": "Bing Bong"}
]
}
}
}
Having a standard and automatic way to limit what’s getting passed around is great. This can also allow clients to not have to deal with:
- metadata they might not care about, or will only care about in special circumstances
- nitpicky implementation details
- any other bits that you just don’t want to be sending around
Receive data in a predictable and consistent format
Let’s say our social network has added something called “Groups” where users can come together. Maybe the data around that is:
type Group {
id: String
name: String
purpose: String
members: [User]
}
And you let clients query for it:
type Query {
user(id: String!): User
group(id: String!): Group
}
And then a client queries for a particular group:
{
group(id: "lees-cool-group") {
name
purpose
}
}
They’ll get:
{
"data": {
"group": {
"name": "The Never Lonely Club",
"purpose": "For people who still have imaginary friends"
}
}
}
Maybe the client asks for a user and a group at the same time:
{
user(id: "leereamsnyder") {
name
}
group(id: "lees-cool-group") {
name
}
}
They’ll receive:
{
"data": {
"user": {
"name": "Lee Reamsnyder"
},
"group": {
"name": "The Never Lonely Club"
}
}
}
You might be sensing the pattern here: a successful GraphQL response always has the same shape.
- A top level
"data"
property, which contains… - The data you asked for…
- In the same shape as the query
If you don’t see the benefits of that, you haven’t lost enough of your life wrangling a lot of varied and inconsistent responses from APIs.
Expose the user-friendly API that you really want
Maybe under the covers, getting a user’s data and getting their list of followers is really two data requests. In a traditional REST API, that might be two HTTP requests to wrangle:
GET /users/:id
GET /users/:id/followers
Our GraphQL API is exposing that in a more natural way, where clients are requesting a user and their followers at the same time—again, only if they want them!
The GraphQL service is still doing those requests (that’s the resolvers portion of the service), but your clients just don’t have to know about those kinds of implementation details, and the client will only make 1 HTTP request, which is probably faster overall.
Mutations are much clearer than your typical REST call
Maybe your clients can update a user’s email address. In REST-world, that might be an HTTP PATCH
on the user
PATCH /user/:id
And you have to send in the body the updated user data:
{
"email": "lee@example.com"
}
That’s… fine. But with REST endpoints you can’t communicate the details:
- What data structure will you accept? Is there some top-level
"user"
property like{"user": {"email": "newemail@example.com"}}
or do you want the new data at the top level? - What limitations are there? Can I only send an email address or can this endpoint update other things?
- What exactly will the API return? The updated user? Just an “OK” response?
Yeah, yeah, that’s what documentation is for. But with GraphQL, you can expose it all explicitly as a mutation:
type Mutation {
updateUserEmail(userId: String!, email: String!): User
}
The updateUserEmail
spells out by name what you’re doing (updating someone’s email). It says exactly what data it needs (the user ID and email address, both strings). It says it will return a User
in the response (the user that we just updated).
It’s much clearer for everyone involved.
Have a single source of data
Maybe your social network has a lot of little dedicated services, and clients have to request data from a few different places. A GraphQL service is a golden opportunity to abstract that away and have the GraphQL service be the single source of data.
The tooling is really incredible
At this point you might be thinking—I know I did at first—“Feh! I will write my own not-buzzword-y API middle layer that does many of these sorts of quality-of-life optimizations!” You sure could!
But before you do that, you should take a look at some of the tooling around GraphQL, because it’s amazing.
Interactive API explorers
A common feature of GraphQL services, like Apollo, is a nifty GUI tool for exploring the API.
Here’s the Apollo server tool with my work project’s API:
I love this thing! I use it for writing my own queries: the autocomplete and error checking inline saves time, and I can do a test-run right in this tool and make sure that it works as I expect.
You also get an interactive list of every query and mutation with all the possible arguments. When another developer asks, "what can our API do?", I can point them at this tool and it’ll tell them everything that it can do and let them play around with it.
Automatic type generation
If you use TypeScript, your brain gears might be turning and you’re thinking: “Huh, a GraphQL schema looks an awful lot like TypeScript types.” Yup! Because a GraphQL service can tell other tools exactly what it can do, there are code-generation tools that you can point at your GraphQL service and output TypeScript types for all the types, queries, and mutations in the schema.
I use the hell out of this. My GraphQL schema is the single source of truth, and I don’t have to write duplicate TypeScript types for the data myself. When I have to change the schema, I regenerate the types and the TypeScript compiler complains immediately so I can fix it. It’s great.
Super smart clients
Apollo is the gold standard here. The Apollo Client has a lot of smarts, either built-in or with plugins:
- The Apollo client has many configurable caching plugins so you can avoid making unnecessary or duplicate requests for data
- The Apollo Client Dev Tools makes it easy to track all the requests that your page is making and helps a ton in when troubleshooting any issues with the cache
- The Batch HTTP plugin will combine multiple simultaneous queries into a single HTTP request automatically. This means you can just write the queries you need in your React component tree, mount them when needed, and the client will automatically fetch them all in the most efficient way. Put another way: it’s automatic optimal data fetching. (!!!)
- The Link Schema plugin lets you easily reuse your entire GraphQL schema for server-side rendering without additional network requests. You can drop your application code, queries included, into a server-rendering scheme and—trust me, I didn’t believe this either—it’ll just work.
There’s more, but that’s the stuff that has really impressed me and that I can personally vouch for.
The React tooling is [chef’s kiss]
The Apollo React library is as comprehensive and easy-to-use as I’ve needed, and it comes in whatever flavor of abstraction (higher-order components, render props, hooks) that you’re into.
Here’s an example React hook for our social network’s groups (the graphql-tag
library turns a query string into a syntax tree the apollo tooling can understand):
import { useQuery } from '@apollo/react-hooks'
import gql from 'graphql-tag'
const GET_GROUP = gql`
query GetGroup($id: String!) {
group(id: $id) {
name
purpose
members: {
name
}
}
}
`
export default function useGroupQuery(id) {
const { error, loading, data, called } = useQuery(GET_GROUP, {
variables: { id },
})
return { error, loading, data, called }
}
[wipes away a tear] It’s so simple!
Check out how the useQuery
hook tracks its loading status, if it’s been called, any errors, and more. If you use Redux and that sort of tracking describes your entire existing Redux store, you can throw that all away. (We did!)
Your clients can stop worrying about boring data plumbing stuff
Although GraphQL is another layer to think about, here’s all the things that my web application doesn’t have to think about any more:
- when I need some data, what path do I need to use?
- what HTTP verb do I need to use?
- do I need to worry about the browser caching this request?
- will the response be plain text or JSON or… anything at all?
- what HTTP response codes do I have to concern myself with?
- do I have to remember to parse the JSON?
- what is the structure of the response?
- what do errors look like?
I do not miss any of this.
What isn’t so great about it?
So, it’s not all magic.
Folks sort of gloss over the whole “resolver” bit

Yeah… so all that neat-o stuff that I just said my client apps don’t have to care about? Errm. The GraphQL service still has to care about those things, because it’s responsible for turning your pretty GraphQL queries into real data and returning it. That’s the “resolvers” portion of the service.
In all likelihood, you’re right back in REST-world and parameterizing routes and managing HTTP codes and parsing JSON and differentiating between multiple types of errors. So there’s no total escape from all that.
However, your clients won’t be dealing with it, and they’ll be requesting data optimally, so this tips over to definitely worth doing for me.
The schema and queries are just strings
Because your schema is just some strings you wrote and your queries are also more strings, you can miss subtle bugs.
For an example, here’s our Query
type again from the schema:
type Query {
user(id: String!): User
}
And here’s a query function that lets you fill in the id
on-demand:
query GetUser($id: String) {
user(id: $id) {
name
}
}
See the error? If you caught that in the schema, the id
is a required string (that’s the !
), but the GetUser
query does not also specify that the $id
argument string is required (no !
), you win!
The error messaging is usually pretty good around this, but those sorts of details are easy to miss when you change your schema. Sure, changing the schema is the root problem, but change is a part of life, man.
It was originally created by Facebook
…yeah.