GraphQL primer part one
In November I'll be giving a talk about GraphQL to the Asheville Javascript Developers group. From schema definition to its use in today's applications. This is part one of three of what will become that talk.
Introduction
I'm in an incredibly fortunate position with my job where I can freely suggest that we approach newer technologies to help solve our problems. One of those technologies, that's on the tip of every developer's tongue these days, is GraphQL. It's not that new. As of right now GraphQL is 3 years old, having been released in 2015. However, even in Asheville you can't throw a stick without hitting at least one dev expounding the merits and sometime frustrations of GraphQL.
These articles are intented to be a primer for anyone new or interested in GraphQL. If you consider yourself adept or masterful in GraphQL you may not find the first few portions of these articles very interesting.
At Firefly XD we manage a rather large React and Redux web application. The vast majority of its interactions with internal services are handled through REST-like endpoints. We don't have a lot of robust paramaterized filtering. Our usuage of GraphQL isn't largely to manage big portions of the application, instead we rely on it (and AWS AppSync) to faciliate our rapid prototyping process. I'll go in to further details about that later.
GraphQL is a data query and manipulation language, and a runtime for handling those queries.
I'm a huge proponent of using whatever technology solves the problem quickly, expressively, and with minimal overhead. Part of the excitement of GraphQL isn't necessarily that it's just new, but that there are powerful services like AppSync and Prisma that faciliate the data access layer leaving the developer to simply write queries. Like any technology it's important to apply a critical lens when approaching it. Services like GraphQL, while useful and full of hype, may not necessarily be the best solution for your problem. There are a great many articles discussing the pros and cons of GraphQL (I strongly suggest searching for them if you're on the fence) and this article is really just intended to support my talk.
Schema: types, queries, and mutations
Your schema defines the shape and relationship of your data. It specifies the type values of fields returned by queries, providing queries and mutations with arguments and their returns. As we'll see later there are services like AppSync and Prisma that can use your schema to help map data to its source.
For our service imagine we're creating a cat adoption agency management application. We need to store adoption agency locations and the cats that they shelter.
Scalar types
When defining your schema each field is assigned a type. GraphQL supports five scalar types Int
, Float
, Boolean
, String
, and ID
. Ending a type definition with an exclaimation mark indicates that the field is non-nullable. Wrapping a type definition in square brackets indicates a list of that type.
name: String # the field name may be null
name: String! # the field name may not be null
cats: [Cats] # the field cats is a list of Cats
Object types
Imagine that we wanted to create a type that described a cat. This cat has a name, a weight, an age, and a breed. To do this we define an object type.
type Cat {
name: String!
age: Int
weight: Float
breed: String
}
Our cat object type has four fields made up of different scalars. The name field is the only field that cannot be null.
type Location {
id: ID!
name: String!
cats: [Cat!]!
}
We want to represent the relationship of an agency location to the cats in their care. I've defined another object type called Location
that has an id, name, and list of cats. Note the placement of the exclaimation mark on [Cat!]!
. This specifies that the field cats will be a non-nullable list that must contain a at least one cat. We should also go ahead and update our cat object to further this relationship. Ensuring that if we only have a cat we can easily resolve at which location the cat is sheltered.
type Cat {
id: ID!
name: String!
age: Int
weight: Float
breed: String
location: Location
}
The cat type will probably need an id field at some point and we may want to resolve the location of an individual cat.
Queries
By convention we generally create an object type called Query that defines a queries for our schema. The definition of a query is much like that of a field on any other object type (in fact fields on any object type can have arguments).
type Query {
location(id: ID!): Location!
cat(id: ID!): Cat!
getLocations(): [Location]!
}
At this point our Query object contains three queries: location
, cat
, and getLocations
. These queries will allow us to get a list of locations, a single location, and a single cat. The stucture of GraphQL allows us to break down our types by field. Whatever exists at the application level can then specify the data it needs.
{
location(id: 42) {
name
cats {
id
name
breed
}
}
}
Writing queries for execution names the fields we want. In this case we're getting the agency name, and the ids, names, and breeds of the cats at location 42.
You may also write a query as follows:
query {
location(id: $id) {
name
cats {
id
name
breed
}
}
}
Named queries
While the regular query syntax works fine for some situations most applications are going to need provided execution context for queries. GraphQL clients and servers provide named queries support. I'll go into more detail about its implementation but it functions largely as a value interpolation mechanism.
query GetLocation($id: ID!) {
location(id: $id) {
name
cats {
id
name
breed
}
}
}
The named query here is GetLocation
and it takes an id value and passes that value into the same location query.
Mutations
In GraphQL a query that changes something is called a mutation. Mutations, much like queries, are defined in an object type named Mutation.
input LocationInput {
name: String!
}
input CatInput {
name: String!
age: Int
weight: Float
breed: String
}
type Mutation {
addLocation(input: LocationInput): Location!
addCat(locationId: ID!, input: CatInput): Cat!
}
Notice that I've also introduced a new organizational type called an input type. Input types allow you to specify multiple fields as an object type passed to a single field. In the case of these two mutations each takes an argument of input that have values mapped to different input types.
Mutation execution
Executing a mutution is similar to the named query above, and like the named queries we will see later that these named mutations are also available through our client.
mutation AddLocation($input: LocationInput!) {
addLocation(input: $input) {
id
name
}
}
mutation AddCat($locationId: ID!, $input: CatInput!) {
addCat(locationId: $locationId, input: $input) {
id
name
breed
}
}
In some cases returning an id or a success boolean on add might be sufficient. Here I've opted to return fields that will be beneficial in updating UI based on the newly added Location and Cat.
Recap and the schema
We've defined two types, two queries, and two mutations that will eventually allow our application to get and add Locations and Cats. All together our schema looks like the following. I've also defined a new schema
type. Many services like AWS AppSync don't require that you provide a schema type. If you have a type named Query or Mutation it will default its execution of the schema to those types.
type Location {
id: ID!
name: String!
cats: [Cat!]!
}
type Cat {
id: ID!
name: String!
age: Int
weight: Float
breed: String
location: Location
}
input LocationInput {
name: String!
}
input CatInput {
name: String!
age: Int
weight: Float
breed: String
}
type Query {
location(id: ID!): Location!
cat(id: ID!): Cat!
getLocations(): [Location]!
}
type Mutation {
addLocation(input: LocationInput): Location!
addCat(locationId: ID!, input: CatInput): Cat!
}
schema {
query: Query
mutation: Mutation
}
There's a great deal more that can be done with GraphQL. I've kepted it simple here to outline the basics.
In part two of this article I'll be creating creating a GraphQL server on top of Prisma and MySQL. Detailing how we can take our current schema, turn it into a relational database schema, and then use Prisma to faciliate the inbetweens of Apollo Server and MySQL.