GraphQL
GraphQL is a query language for APIs. It can be used to describe queries and mutations which do exactly what the consumer wants, in a way that mirrors the way the data is returned by the API, in a way that removes the need for multiple round-trip requests.
Queries
Queries have the same shape as their result.
{
hero {
name
}
}
{
"data": {
"hero": {
"name": "R2-D2"
}
}
}
Sub-selection of fields can be made in queries:
{
hero {
name
# Queries can have comments!
friends {
name
}
}
}
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
Arguments
Fields can take arguments, even scalar fields. Arguments themselves may be of any type.
{
human(id: "1000") {
name
height(unit: FOOT)
}
}
{
"data": {
"human": {
"name": "Luke Skywalker",
"height": 5.6430448
}
}
}
Aliases
Aliases can be used to query the same field with different arguments:
{
empireHero: hero(episode: EMPIRE) {
name
}
jediHero: hero(episode: JEDI) {
name
}
}
{
"data": {
"empireHero": {
"name": "Luke Skywalker"
},
"jediHero": {
"name": "R2-D2"
}
}
}
Fragments
Fragments can name a set of fields to allow them to be reused within queries:
{
leftComparison: hero(episode: EMPIRE) {
...comparisonFields
}
rightComparison: hero(episode: JEDI) {
...comparisonFields
}
}
fragment comparisonFields on Character {
name
appearsIn
friends {
name
}
}
{
"data": {
"leftComparison": {
"name": "Luke Skywalker",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"friends": [
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
},
{
"name": "C-3PO"
},
{
"name": "R2-D2"
}
]
},
"rightComparison": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
Operation Name
Previous syntax omits the query
keyword specifying the operation type and the query name specifying the operation name. Operation types can be a query, mutation, or subscription.
query HeroNameAndFriends {
hero {
name
friends {
name
}
}
}
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
Variables
Queries can be given arguments, preventing the need to interpolate parameters into query strings, similar to prepared statements in SQL. Variables can be optional or required, and must be scalars, enums, or input types. Default values can be assigned after the type declaration.
query HeroNameAndFriends($episode: Episode = "JEDI") {
hero(episode: $episode) {
name
friends {
name
}
}
}
{
"episode": "JEDI"
}
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
Directives
Directives can be attached to a filed or fragment inclusion to affect the execution of the query. Core GraphGL defines two directives:
@include(if: Boolean)
- only include this field if the argument is true@skip(if: Boolean)
- skip this field if the argument is true
Mutations
By convention, operations that cause writes should be sent explicitly via a mutation. The main distinction between queries and mutations beyond the name is that query fields are executed in parallel, whereas mutation fields are run in series. For example, if two incrementCredits
mutations are sent in one request, the first one is guaranteed to finish before the second begins.
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}
{
"ep": "JEDI",
"review": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
{
"data": {
"createReview": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
}
Inline Fragments
Inline fragments are used to access the data of an underlying concrete type of a returned interface or union type. Named fragments can also be used like this.
Below, the hero
field may return a Character
interface of type Human
or Droid
depending on the episode
argument. With direct selection, only fields that exist on the Character
interface, such as name
. A type condition of form ... on Type
is used with an inline fragment to access fields from a concrete type. For example, below the primaryFunction
field will only be executed if the Character
is a Droid
type.
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
... on Droid {
primaryFunction
}
... on Human {
height
}
}
}
{
"ep": "JEDI"
}
{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
The __typename
meta field can be used to get the name of the object type at that point. It is particularly useful for returned union types.
{
search(text: "an") {
__typename
... on Human {
name
}
... on Droid {
name
}
... on Starship {
name
}
}
}
{
"data": {
"search": [
{
"__typename": "Human",
"name": "Han Solo"
},
{
"__typename": "Human",
"name": "Leia Organa"
},
{
"__typename": "Starship",
"name": "TIE Advanced x1"
}
]
}
}
Schema
Queries are validated and executed against the schema.
Objects
An object type may look like this. A non-nullable type is represented with an exclamation mark !
suffix. Arrays are represented with bracket []
syntax.
type Character {
name: String!
appearsIn: [Episode]!
}
Every field in an object can have arguments which are always named and may have a default value.
type Starship {
id: ID!
name: String!
length(unit: LengthUnit = METER): Float
}
Query and Mutation Types
The schema contains two special types Query
and Mutation
which are regular object types which define the entry point of every GraphQL query and mutation. The fields contained within them define the root query and mutation fields.
They are only special in being entry points into the schema, but are otherwise the same as any other object type. They can be named anything else, but are named Query
and Mutation
by convention.
schema {
query: Query
mutation: Mutation
}
type Query {
hero(episode: Episode): Character
droid(id: ID!): Droid
}
query {
hero {
name
}
droid(id: "2000") {
name
}
}
Scalars
The default scalar types include:
Type | Definition |
---|---|
Int |
32-bit signed integer |
Float |
signed double-precision floating-point value |
String |
UTF-8 string |
Boolean |
true or false |
ID |
serialized as String , but not intended to be human-readable |
Most GraphQL implementations have a way to create custom scalar types. The implementation then defines how the type should be serialized, deserialized, and validated.
scalar Date
Enumerations
In GraphQL enumerations are a set of pre-defined values. It’s up to the implementation to determine how they’re represented.
Type Modifiers
A type can be marked as non-null by adding the exclamation mark !
suffix. During validation, if the server gets a null value for a non-null field, it will trigger a GraphQL execution error. It can also be applied to a field argument.
myField: [String!]
myField: null // valid
myField: [] // valid
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // error
myField: [String]!
myField: null // error
myField: [] // valid
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // valid
Interfaces
An interface is an abstract type that includes a certain set of fields that a type must include to satisfy it, but they may also include additional fields.
interface Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
}
type Human implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
starships: [Starship]
totalCredits: Int
}
type Droid implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
primaryFunction: String
}
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
... on Droid {
primaryFunction
}
}
}
{
"ep": "JEDI"
}
{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
Unions
The members of a union type need to be concrete object types. Unions can’t be created from interfaces or other unions.
union SearchResult = Human | Droid | Starship
Inline fragments can be used to query any of the union members.
{
search(text: "an") {
... on Human {
name
height
}
... on Droid {
name
primaryFunction
}
... on Starship {
name
length
}
}
}
{
"data": {
"search": [
{
"name": "Han Solo",
"height": 1.8
},
{
"name": "Leia Organa",
"height": 1.5
},
{
"name": "TIE Advanced x1",
"length": 9.2
}
]
}
}
Input Types
Input types look the same as regular object types but use input
instead of type
. The fields of an input type can refer to other input types, but can’t mix input and output types, nor can they have arguments on their fields.
According to the standard, input types are necessary because object types may contain fields that express circular references or references to interfaces and unions, which aren’t appropriate as an input argument.
Input types are used as query parameters, whereas output types are obtained after query execution.
Naming input types with an Input
suffix is a common convention due to the common practice of having regular output object types and a corresponding input type.
input ReviewInput {
stars: Int!
commentary: String
}
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}
{
"ep": "JEDI",
"review": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
{
"data": {
"createReview": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
}
Execution
Each field on each type is backed by a function called a resolver. When a field is executed, the resolver is called to produce the next value. Execution stops when the query reaches the leaves of the query corresponding to scalar values.
At the top level of a GraphQL server schema is a type known as the Root type or Query type that represents all possible entry points into the API.
Resolvers
The resolver function for a human
field on the Query
type may look like this. The obj
parameter refers to the previous/parent object, which may not be used on the root type. The args
parameter contains the arguments provided to the field in the query. The context
value is provided to every resolver and holds contextual information such as the currently logged in user or database connection.
Query: {
human(obj, args, context) {
return context.db.loadHumanByID(args.id).then(
userData => new Human(userData)
)
}
}
The resolver function for a Human
object’s name
field may look like this. In this case, the obj
argument is the new Human
returned from the previous Human
field. GraphQL libraries may assume missing resolvers to simply be properties of the same name that should be read and returned, as is the case with the name
field resolver.
Human: {
name(obj, args, context) {
return obj.name
}
}
The type system may use scalar coercion to convert values returned by a resolver function into something that matches the API, such as numbers used internally being coerced into enumeration values.
GraphQL waits for all promises concurrently before continuing, even those found in a list.
As each scalar field is resolved, it’s placed into a key-value map with the field name or alias as the key and the resolved value as the value, in a bottom-up fashion back up to the root Query type.
Introspection
The __schema
field is always available on the root type of a Query
and can be queried to obtain the schema of an API. The types { name }
query includes the defined types, the built-in scalars, and the types that are part of the introspection system.
{
__schema {
types {
name
}
}
}
{
"data": {
"__schema": {
"types": [
{
"name": "Query"
},
{
"name": "Episode"
},
{
"name": "Character"
},
{
"name": "ID"
},
{
"name": "String"
},
{
"name": "Int"
},
{
"name": "FriendsConnection"
},
{
"name": "FriendsEdge"
},
{
"name": "PageInfo"
},
{
"name": "Boolean"
},
{
"name": "Review"
},
{
"name": "SearchResult"
},
{
"name": "Human"
},
{
"name": "LengthUnit"
},
{
"name": "Float"
},
{
"name": "Starship"
},
{
"name": "Droid"
},
{
"name": "Mutation"
},
{
"name": "ReviewInput"
},
{
"name": "__Schema"
},
{
"name": "__Type"
},
{
"name": "__TypeKind"
},
{
"name": "__Field"
},
{
"name": "__InputValue"
},
{
"name": "__EnumValue"
},
{
"name": "__Directive"
},
{
"name": "__DirectiveLocation"
}
]
}
}
}
The different types of queries can be obtained with the queryType
field.
{
__schema {
queryType {
name
}
}
}
A specific type can be queried:
{
__type(name: "Droid") {
name
kind
}
}
Fields can be queried with the fields
field.
{
__type(name: "Droid") {
name
fields {
name
type {
name
kind
}
}
}
}
A list kind is the LIST
wrapper type. The ofType
field can be used to obtain the wrapped type.
{
__type(name: "Droid") {
name
fields {
name
type {
name
kind
ofType {
name
kind
}
}
}
}
}
A type’s documentation can be obtained with the description
field.
{
__type(name: "Droid") {
name
description
}
}
{
"data": {
"__type": {
"name": "Droid",
"description": "An autonomous mechanical character in the Star Wars universe"
}
}
}
Best Practices
Versioning
GraphQL doesn’t really need to be versioned because clients only request what they request, and new types and fields without breaking existing clients.
Batching
Naive resolvers may incur many redundant queries. A common optimization is to debounce and batch them into single requests by using something like DataLoader or join-monster.
Pagination
A suggested way to implement pagination is to use cursors at an additional level of indirection representing the edge between a type and its list of associated items. The edge would return the item itself within a node
field as well as a cursor
field. The plural field can contain cumulative information such as totalCount
, as well as a pageInfo
field which can contain information about the startCursor
and endCursor
on that page, as well as whether it hasNextPage
to avoid the final query.
This is known as a Relay Cursor Connection.
{
hero {
name
friendsConnection(first:2 after:"Y3Vyc29yMQ==") {
totalCount
edges {
node {
name
}
cursor
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
{
"data": {
"hero": {
"name": "R2-D2",
"friendsConnection": {
"totalCount": 3,
"edges": [
{
"node": {
"name": "Han Solo"
},
"cursor": "Y3Vyc29yMg=="
},
{
"node": {
"name": "Leia Organa"
},
"cursor": "Y3Vyc29yMw=="
}
],
"pageInfo": {
"endCursor": "Y3Vyc29yMw==",
"hasNextPage": false
}
}
}
}
}
GraphQL.js
A minimal GraphQL servers looks like this.
var { graphql, buildSchema } = require('graphql');
// Construct a schema, using GraphQL schema language
var schema = buildSchema(`
type Query {
hello: String
}
`);
// The root provides a resolver function for each API endpoint
var root = {
hello: () => {
return 'Hello world!';
},
};
// Run the GraphQL query '{ hello }' and print out the response
graphql(schema, '{ hello }', root).then((response) => {
console.log(response);
});
It’s possible and straightforward to define an object type resolver as an instance of an ES6 class such that its instance methods and properties are resolvers.
type RandomDie {
numSides: Int!
rollOnce: Int!
roll(numRolls: Int!): [Int]
}
type Query {
getDie(numSides: Int): RandomDie
}
class RandomDie {
constructor(numSides) {
this.numSides = numSides;
}
rollOnce() {
return 1 + Math.floor(Math.random() * this.numSides);
}
roll({numRolls}) {
var output = [];
for (var i = 0; i < numRolls; i++) {
output.push(this.rollOnce());
}
return output;
}
}
var root = {
getDie: function ({numSides}) {
return new RandomDie(numSides || 6);
}
}