Learn why GraphQL is all the rage! We’ll walk through the implementation of a schema for a popcorn company’s API, learning about types, queries, and mutations as we go.
I love popcorn.
As I’m writing this, I’m snacking on a bowl of multi-colored kernels, lightly seasoned with Flavacol (hint: this is the movie theater’s secret) and I’m feeling a twinge of hubris in my eye…
“I should start a popcorn company…”
Well, that’s a terrible idea, Ryan! But, for the sake of this tutorial, it’s the best idea you’ve had all week. In this tutorial, we’re going to learn how to create a simple, GraphQL-based API for a popcorn company.
All we need is a name. Seeing as how I’m quite fond of sarcasm, how about…
Sarcastic Kernels: The Popcorn You Love to Hate to Eat™
Perfect! And like any self-respecting nerd, it only seems right to start writing some code before we come up with a sound business plan. To get started, let’s wrap our heads around what GraphQL is and how it compares to REST.
What is GraphQL?
GraphQL is a client-side query language coupled with a pattern — formally known as a “schema” — for organizing the creation, reading, updating, and deleting of data in your application (yeah, that CRUD).
We say “application”, here, and not database because GraphQL is data-source-agnostic, meaning it doesn’t care where your data lives.
From the outside looking in, GraphQL can seem quite scary. Does the “Graph” part mean I have to learn about graph databases? Does the QL (query language) mean I have to learn I have to learn a brand new programming language?!
Not quite. To calm your nerves, the harsh truth is: GraphQL is just a dressed up GET
or POST
request.
Wait, what?!
Yep. While GraphQL as a whole does introduce some new concepts for organizing and interacting with your data, behind the curtain, GraphQL still relies on a good ol’ fashioned HTTP request to do its magic.
Rethinking REST
Where GraphQL separates from a more familiar REST API is due to its flexibility. With REST, done properly, endpoints are typically designed from the perspective of a resource, or, a type of data in our application.
For example, a GET
request to /api/v1/flavors
would be expected to send us back a response that looks something like this:
[
{
"id": 1,
"name": "The Lazy Person's Movie Theater",
"description": "That elusive flavor that you begrudgingly carted yourself to the theater for, now in the comfort of your own home, you slob!"
}, {
"id": 2,
"name": "What's Wrong With You Caramel",
"description": "You're a crazy person that likes sweet popcorn. Congratulations."
}, {
"id": 3,
"name": "Gnarly Chili Lime",
"description": "The kind of popcorn you make when you need a good smack in the face."}
]
Now, there’s nothing terribly wrong with this, but let’s consider our UI, or rather, how we intend to consume this data.
If we wanted to display a simple list UI where all we had were the kinds of popcorn that are available (and nothing else), we might end up with a design like the one below.
And… we’d be in a bit of a jam. We can choose not to use the description
field, sure, but are we just going to sit around and act like we didn’t send that to the client as well?!
Haha! What do you think this is? Fantasy land? Of course we are! And when someone asks “why is the app so slow for users?” in a few months, we’ll just say “be right back, going to grab a coffee!” and then split town, never to be seen again.
It’s not entirely our fault, though. To be fair, REST is the data-fetching equivalent of going to a restaurant and being asked “What do you want? We’ll give you what we have.”
Jokes aside, in a real application this can be problematic. For example, we may display different traits for each kind of popcorn like pricing information, brand details, or dietary restrictions (“vegan popcorn!”). Rigid REST endpoints make delivering specific traits based on context a headache, leading to unnecessary performance overhead and frustration.
How GraphQL improves REST
On the surface, this may seem like a small problem. “Who cares if we’re sending unnecessary data to the client?” Well, let’s add some context. GraphQL was invented at Facebook. Facebook serves millions of requests per second.
Translation? Every optimization counts.
Instead of saying “here’s what’s available,” GraphQL inverts the problem and asks “what do you need?”
We can get back a response from GraphQL specific to the context where we’re consuming data without having to add a one-off endpoint, perform multiple requests, or write convoluted conditional code.
How does GraphQL work?
Like we hinted at above, at it’s core GraphQL relies on a simple GET
or POST
request for moving data to and from the client. Unpacking that, GraphQL has two types of requests when it comes to reading (the R in CRUD), creating, updating, and deleting (The CUD in CRUD): queries and mutations.
All of those queries and mutations are sent as either a GET
or POST
request to a GraphQL server at a URL like https://myapp.com/graphql
. More on that below.
Understanding Queries
Queries are what you’d expect: a request for some data. We have the UI, and we need to fill it with data, so we make a query to the server. With a traditional REST API, our query would come in the form of a GET request. With GraphQL, we introduce a new syntax for requesting data:
{
flavors {
name
}
}
Wait, you mean JSON? Or is that a JavaScript object?
Neither.
The “QL” part of GraphQL stands for query language. Quite literally, this is a brand new language for writing data queries. That sounds more complicated than it is. Let’s break down the above query.
{
// The fields we want to query are written here.
}
All queries start from the “root query” and are known as fields. To save yourself a headache: it’s best to refer to this as “query fields in my schema.” Don’t fret, we’ll learn more about how those are defined in a bit. Here, we ask to query the flavors
field on the root query.
{
flavors {
// The sub-fields we want for each flavor are written here.
}
}
When querying a field, we also need to specify the sub-fields we want for each object in the response (even if we expect just a single object to be returned).
{
flavors {
name
}
}
The end result? When we send this query to a GraphQL server, we get back a neat, tidy response like this:
{
"data": {
"flavors": [
{ "name": "The Lazy Person's Movie Theater" },
{ "name": "What's Wrong With You Caramel" },
{ "name": "Gnarly Chili Lime" }
]
}
}
Neat, right? To make this clear, if we ran the following query on another page:
{
flavors {
id
name
description
}
}
we’d get back a response like this:
{
"data": {
"flavors": [
{ "id": 1, "name": "The Lazy Person's Movie Theater", description: "That elusive flavor that you begrudgingly carted yourself to the theater for, now in the comfort of your own home, you slob!" },
{ "id": 2, "name": "What's Wrong With You Caramel", description: "You're a crazy person that likes sweet popcorn. Congratulations." },
{ "id": 3, "name": "Gnarly Chili Lime", description: "A friend told me this would taste good. It didn't. It burned my kernels. I haven't had the heart to tell him." }
]
}
}
Super powerful! Same endpoint, with a different response tailored to the context.
If we wanted to get a single flavor, GraphQL queries accept arguments, too:
{
flavors(id: "1") {
id
name
description
}
}
Here, we’ve hardcoded the specific id
of a flavor we want to query, but we can also have a dynamic id
:
query getFlavor($id: ID) {
flavors(id: $id) {
id
name
description
}
}
On the first line, we give our query a name (this is arbitrary; we could replace getFlavor
with pizza
and this would still work) and define the variables that query expects. Here, we expect a possible variable id
to be passed as an ID
scalar type (more on these below).
Regardless of using a static or dynamic id
to make our request, here’s the response we can expect:
{
"data": {
"flavors": [
{ "id": 1, "name": "The Lazy Person's Movie Theater", description: "That elusive flavor that you begrudgingly carted yourself to the theater for, now in the comfort of your own home, you slob!" }
]
}
}
Nice! Hopefully your hamster is starting to spin. This is already great, but where GraphQL really starts to shine is with nested fields. Let’s assume we had another field in our schema called nutrition
that told us just how unhealthy our sarcastic kernels are:
{
flavors {
id
name
nutrition {
calories
fat
sodium
}
}
}
What this may look like is us having a nested nutrition
object on each of our flavors
. Not quite! With GraphQL, we can combine separate but related data sources into a single query, getting a response that gives us the benefit of nested data without having to denormalize everything in the database:
{
"data": {
"flavors": [
{
"id": 1,
"name": "The Lazy Person's Movie Theater",
"nutrition": {
"calories": 500,
"fat": 12,
"sodium": 1000
}
},
...
]
}
}
That’s a serious boon on productivity. But what about updating data, does GraphQL give the same advantages?
Understanding Mutations
Where queries fetch data, mutations are responsible for making changes to data. Alternatively, too, mutations can be used for a generic RPC (remote procedure call) for miscellaneous tasks like sending a user’s data to a third-party API.
mutation updateFlavor($id: ID!, $name: String, $description: String) {
updateFlavor(id: $id, name: $name, description: $description) {
id
name
description
}
}
Mutations rely on a similar syntax to queries. Here, we define a mutation updateFlavor
with some variables: id
, name
, and description
. Just like with our queries, we “wrap” a mutation field (defined on a similar root mutation) using the keyword mutation
, followed by a name describing the mutation and a set of variables to pass along.
Those variables include what we’re trying to change or mutate. Notice, too, that after executing a mutation, we can ask for some fields back.
In this case, we want to get back the id
, name
, and description
after they’ve been mutated. This helps with things like optimistic UI, negating the need for a request following the update.
Writing a schema and attaching it to a GraphQL server
So far, what we’ve been looking at is how GraphQL functions on the client. This is how we make requests, but how do we respond to them?
The GraphQL server
In order to make a GraphQL request, we need to have a GraphQL server to send it to. A GraphQL server is a regular ol’ HTTP server (if you’re a JavaScripter, think Express or Hapi) with a GraphQL schema attached to it.
import express from 'express'
import graphqlHTTP from 'express-graphql'
import schema from './schema'
const app = express()
app.use('/graphql', graphqlHTTP({
schema: schema,
graphiql: true
}))
app.listen(4000)
By “attached,” we mean that requests received by that server are passed through the schema and then back to the client, kind of like an air filter in your house.
The “filtering” process that takes place is relative to the query or mutation you send from the client. Both queries and mutations are resolved using functions associated with the fields defined on our root query or root mutation in our schema.
Above, we can see a mock HTTP server being created with the JavaScript library Express. Utilizing the graphqlHTTP
function from the express-graphql
package by Facebook, we “attach” our schema (here, assumed to be defined in another file) and start our server on port 4000
(i.e., http://localhost:4000/graphql
is where we’ll send requests from the client).
Types and resolvers
With a running server, we need to define the schema that we attach to it.
Recall that earlier, we talked about defining fields on either a root query or root mutation.
import gql from 'graphql-tag'
import mongodb from '/path/to/mongodb’ // For example. Assuming `mongodb` gives us a MongoDB connection.
const schema = {
typeDefs: gql`
type Nutrition {
flavorId: ID
calories: Int
fat: Int
sodium: Int
}
type Flavor {
id: ID
name: String
description: String
nutrition: Nutrition
}
type Query {
flavors(id: ID): [Flavor]
}
type Mutation {
updateFlavor(id: ID!, name: String, description: String): Flavor
}
`,
resolvers: {
Query: {
flavors: (parent, args) => {
// Assuming args equals an object like { id: '1' }
return mongodb.collection('flavors').find(args).toArray()
},
},
Mutation: {
updateFlavor: (parent, args) => {
// Assuming args equals an object like { id: '1', name: 'Movie Theater Clone', description: 'Bring the movie theater taste home!' }
// Perform the update.
mongodb.collection('flavors').update(args)
// Return the flavor after the update.
return mongodb.collection('flavors').findOne(args.id)
},
},
Flavor: {
nutrition: (parent) => {
return mongodb.collection('nutrition').findOne({
flavorId: parent.id,
})
}
},
},
}
export default schema
When it comes to defining fields in a GraphQL schema, there are two parts: typeDefs
and resolvers
.
typeDefs
contain the type definitions for the data in our application. For example, earlier we talked about retrieving a list of flavors
. In order to do that, we need to do three things:
- Tell our schema what a flavor’s data looks like (above, by defining the
type Flavor
type). - Define a field on the root
type Query
field (above, theflavors
property on thetype Query
value). - Define a resolver function on the
resolvers.Query
object corresponding to the field we defined on the roottype Query
field.
Focusing on the typeDefs
, this is where we tell our schema about the shape of our data. In other words, we tell GraphQL about the different properties a piece of data might contain.
type Flavor {
id: ID
name: String
description: String
nutrition: Nutrition
}
The type Flavor
definition says that “a flavor can contain an id
as an ID
, a name
as a String
, a description
as a String
, and nutrition
as Nutrition
.”
For that last one, nutrition
, we pass the name of another type defined in our typeDefs
. Here, type Nutrition
describes how nutrition data is shaped in our application.
Notice that we’re not saying in our database. A database is assumed in the example above, but your data can come from any data source. Even a third-party API or a static file!
Just like we did for type Flavor
we specify the names of the field a piece of nutrition
data will have, assigning what GraphQL refers to as scalar types to each property. As of writing, GraphQL recognizes five built-in scalar types:
Int
: A signed 32‐bit integer.Float
: A signed double-precision floating-point value.String
: A UTF‐8 character sequence.Boolean
:true
orfalse
.ID
: A unique identifier, often used to refetch an object or as the key for a cache. The ID type is serialized in the same way as a String; however, defining it as an ID signifies that it is not intended to be human‐readable.
In addition to these scalar types, we can also assign custom types to a property like we did with the Nutrition
type on the nutrition
property of the type Flavor
above.
type Query {
flavors(id: ID): [Flavor]
}
On our root type Query
(the “root query” we talked about earlier), we define the name of a field that can be queried. When defining that field, too, we specify any arguments we might expect along with the type of data we expect to be returned.
In this example, we expect a possible id
argument to be passed as an ID
scalar type and expect an array of objects resembling the Flavor
type in response.
Wiring up a query resolver
With our flavors
field defined on our root type Query
, next, we define what’s known as a resolver function.
This is where GraphQL more or less “stops.” If we look in the resolvers
object of our schema file and then in the Query
object nested under that, we can see a property flavors
assigned to a function. This flavors
here is the resolver function for the flavors
field defined on our root type Query
.
typeDefs: gql`…`,
resolvers: {
Query: {
flavors: (parent, args) => {
// Assuming args equals an object like { id: '1' }
return mongodb.collection('flavors').find(args).toArray()
},
},
…
},
This resolver function takes a few different arguments: the parent
query if one exists, the args
passed to the query if any exist, and a missing context
argument which gives us miscellaneous “context” data (e.g., the current user if we provide them when our server starts).
Inside our resolver, we do whatever we need to do to resolve the query. This is where GraphQL “quits caring” and leaves retrieving and returning data up to us. Again, this could be a call to a database, an API, a static file…anything.
While GraphQL doesn’t care where our data comes from, it _does _ care about what we return. We can return a JSON object, an array of JSON objects, or a Promise (which GraphQL will resolve for us).
Here, we use a mock call to a MongoDB database collection called flavors
, passing in our args
(if any exist) to a .find()
call and returning what it finds as an array.
Resolving nested fields
What may not be clear above is how our nested nutrition
data is resolved. Remember: we’re not actually storing the nutrition data on each flavor
but assume it lives in another database collection/table.
While we did tell GraphQL that our type Flavor
might include some nutrition
data in the shape of the type Nutrition
, we didn’t explain how to actually resolve that data. Again: the nutrition
data for a flavor is assumed to be in a different collection than the flavor data.
typeDefs: gql`
type Nutrition {
flavorId: ID
calories: Int
fat: Int
sodium: Int
}
type Flavor {
[…]
nutrition: Nutrition
}
type Query {…}
type Mutation {…}
`,
resolvers: {
Query: {
flavors: (parent, args) => {…},
},
Mutation: {…},
Flavor: {
nutrition: (parent) => {
return mongodb.collection('nutrition').findOne({
flavorId: parent.id,
})
}
},
},
If we look close at the resolvers
object on our schema, notice that we have Query
, Mutation
, and Flavor
. These correspond to the types we defined in the typeDefs
above.
Looking at the Flavor
object, we can see the field nutrition
being defined as a resolver function. What’s unique about this is that we’re defining this _on the Flavor
type directly. In other words, we’re saying “this is how we want you to resolve the nutrition
field for any queries utilizing the type Flavor
.”
Inside, we do a familiar MongoDB query, but notice that we utilize the parent
argument passed to the resolver function. The parent
here is each iteration of the flavors
field. For example, if we ask for all flavors at once like this:
{
flavors {
id
name
nutrition {
calories
}
}
}
For each flavor
returned by flavors
, we’d pass it through the nutrition
resolver defined on resolvers.Flavor
as parent
. If we look close, we can see that we utilize the parent.id
field, referring to the id
of the flavor we’re currently iterating (looping) over.
We pass that parent.id
to our database query, matching it to a (presumed) flavorId
property on each nutrition
item.
Wiring up mutations
Conveniently, our knowledge of wiring up queries maps over perfectly to mutations. In fact, the process is nearly identical. If we take a look at our root type Mutation
, we can see that we define a field updateFlavor
accepting the arguments we specified on the client:
type Mutation {
updateFlavor(id: ID!, name: String, description: String): Flavor
}
Here, we’re saying “we expect the updateFlavor
mutation to accept a possible id
as an ID
(the !
tells GraphQL that this is required), name
as a String
, and description
as a String
.” Additionally, once our mutation completes, we expect some data in return resembling the Flavor
type (i.e., containing an id
, name
, description
, and/or nutrition
).
{
typeDefs: gql`…`,
resolvers: {
Mutation: {
updateFlavor: (parent, args) => {
// Assuming args equals an object like { id: '1', name: 'Movie Theater Clone', description: 'Bring the movie theater taste home!' }
// Perform the update.
mongodb.collection('flavors').update(
{ id: args.id },
{
$set: {
...args,
},
},
)
// Return the flavor after the update.
return mongodb.collection('flavors').findOne(args.id)
},
},
},
}
Inside our resolver function for the updateFlavor
mutation, we do what you might expect: interact with our database to change or update the flavor.
Notice that immediately after we perform the update, we make a call back to our database to find the same flavor again and return it from our resolver. Why?
Remember: on the client, we expect a return value after our mutation completes. In this example, we expect the flavor
we just updated to be returned.
Couldn’t we just return the args
object? Yep! We could. The reason we choose not to in this case is that we want to be 100% certain our database update succeeded. If we go fetch the data again and see that it’s changed: all is well!
Why would I want to use GraphQL?
Though it may not look like much, at this point we have a functioning—albeit simple—GraphQL API up and running!
As with any new tech, though, what may not be abundantly clear is why you’d even want to use this. To be fair, this is a lot of moving parts. Why shouldn’t we just stick to REST or talking to the database directly?
You want to reduce the number of requests made from the client
Where a lot of apps get bogged down is in the number, frequency, and complexity of HTTP requests. While GraphQL doesn’t completely eliminate requests, utilized properly, it can significantly reduce the number of requests you make from the client (in many cases, down to just one).
Whether you’re running an app with tons of users or an app with lots of data (e.g., an app for handling medical records), using GraphQL will definitely speed up client performance.
You want to avoid denormalizing data just to compensate the UI
In applications with a lot of relational data, the “denormalization trap” can be quite common. While this works, it’s by no means ideal and can slow things down unnecessarily. With GraphQL and nested queries, the need to denormalize your data is significantly reduced.
You have multiple data sources to talk to from different apps
This problem can be solved in part with a traditional REST API, but it still leaves a problem: consistent querying from the client. Assuming you have a product with a web app, iOS app, Android app, and developer API, it’s likely that you’ll have to rig up querying for each of those platforms.
This translates to developing knowledge of multiple client implementations for HTTP requests, an inconsistent means for performing queries, and messy, platform-specific endpoints in your API (don’t you “holier than thou” me, friend, you know you’ve done this before!).
Is GraphQL perfect? Should I ditch my REST API today and switch to it?
Of course not! Nothing is perfect.
Where GraphQL most obviously falls short is its complexity. Writing a GraphQL schema introduces a lot of mandatory steps in order to wire up your data. As you’re learning, this can be frustrating because what is missing from your schema may not always be obvious and errors on the client and server may be unhelpful.
Further, consuming GraphQL on the client is not standardized beyond the GraphQL query language. Different libraries exists for this, the most popular being Apollo and Relay, each with their own idiosyncrasies.
GraphQL is also just a specification. Packages like graphql
(used internally by the express-graphql
package from our examples) are just an implementation of that specification. In other words, different implementations for different programming languages may interpret the specification differently. This can lead to problems for you and your team if you use multiple languages across projects.
GraphQL is an impressive step forward in handling data, though. It’s by no means a silver bullet, but it’s certainly worth experimenting with. A great way to get started is to think about a particularly messy part of your data process and try to implement it using GraphQL.
This is the great news: GraphQL can be implemented incrementally. You don’t have to go all in to start taking advantage of it. This is a great way to get buy in from your team and stakeholders and create an opportunity to get your hands dirty.
Keep in mind: GraphQL is ultimately just a tool for doing a job. It’s not “killing” anything. That said, it’s worth familiarizing yourself with it and starting to apply in your apps. Anywhere you struggle with performance overhead or complex UIs like data dashboards, news feeds, and user profiles is a great place to get started.
Happy coding!
Comments