Special thanks to Sebastian Siemssen (fubhy) for collaboration on the material presented for the Acquia webinar.
Preston So has designed and developed websites since 2001 and built them in Drupal since 2007. He is Development Manager of Acquia Labs at Acquia and co-founder of the Southern Colorado Drupal User Group (est. 2008). Previously, Preston was Technical Lead of Entertainment Weekly at Time Inc.
GraphQL is a declarative query language and an application-level protocol.
React is a view-based JavaScript framework that uses a Virtual DOM to save application state and rerenders components of the view based on diffed states.
Relay is a framework that juxtaposes data fetching needs and React components in the same place; in other words, Relay connects view logic to queries in the back end.
GraphQL is a query language that enables transport- and storage-agnostic communication and returns data according to the same structure as the request query.
Instead of placing data fetching logic in some other part of the client application – or embedding this logic in a custom endpoint on the server – we instead co-locate a declarative data-fetching specification alongside the React component.
—Nick Schrock, Facebook
Let’s take a React application that needs to fetch some fields adhering to a certain structure, such as a list of articles, each alongside the following information.
React consists of components, which are rendered through JSX templates and comprise view hierarchies.
var articleListItem = React.createClass({
render: function () {
return (
{this.props.article.title}
{this.props.article.author.name}
from {this.props.article.author.location}
);
}
});
With GraphQL, this structure can be easily mirrored on the server in both query and response.
var articleListItem = React.createClass({
statics: {
queries: {
article: function () {
return graphql`
{
article {
title
path
author {
name
location
}
}
}
`;
}
}
}
// render: function () { ...
});
There's a lot of power in this approach ...
{
article {
title
path
author {
name
location
}
}
}
... because the server’s response really does mirror the client’s desired query.
{
"article": {
"title": "Gang Jailed: Hoverboard Rampage Destroys Courthouse",
"path": "http://usatoday.com/article/gang-jailed-hoverboard-rampage",
"author": {
"name": "Compu-Fax",
"location": "Hill Valley, CA"
}
}
}
Endpoints are specific to individual views or ill-suited for trees of related resources, leading to bespoke or ad-hoc endpoints.
Due to a REST API’s changing requirements, clients that only need limited data may have to deal with increasingly large payloads destined for other clients.
GET /users/:id
GET /groups/:id
GET /groups/:id/users/:id
If what is fetched for your view is complex and relational, multiple client-server round trips and corresponding bootstraps will be required, delaying render.
REST APIs are versioned and by nature obsolescent as client needs evolve. Queries must be rewritten against the new API.
REST APIs with custom endpoints usually lack a native schema or formalized type system, making client-side tooling and validation difficult.
A shared endpoint can resolve GraphQL queries into root calls and send back a single, unified response.
Client-driven queries mean that the response is catered to the demands of the client rather than the limitations of the API.
A GraphQL server returns a single response to match the request structure, which is flexible enough to accommodate many possible relationships.
No matter what version your most current API, your client can provide the same query to multiple versions and expect a common response.
GraphQL has a native and highly extensible schema and type system.
GraphQL is a common vernacular between server and client, similar enough to JSON to be easy for onboarding.
GraphQL accelerates the arrival of your query’s response by preventing unneeded requests or bootstraps of the back end.
GraphQL is an application-layer protocol agnostic to both its transport (the HTTP protocol is only one of many ways queries can be transmitted) and the database queried (schemas need not match).
Source: GraphQL with Nick Schrock, React London meetup (Oct. 2015)
Different requirements for views can be satisfied by provisioning additional REST endpoints for small subsets of data. But this arbitrarily adds more endpoints.
HTTP can handle multiple parallel network requests. But HTTP/1.1 recommends only two, while Chrome accepts six. HTTP/2 may mitigate this.
Content negotiation in HTTP allows clients to request multiple versions of a document at a single path. This resolves the issue of requests against API versions at differing paths.
HTTP has a built-in content type system that is analogous to GraphQL’s native type system and can accompany standards such as JSON Schema.
Rising Stack’s GraphQL server is implemented in Node.js and has MongoDB integration: github.com/RisingStack/graphql-server
GraphQL models two types of operations.
query {
# Read-only fetch
}
mutation {
# Write then fetch
}
These operations can have names, which are case-sensitive.
query getUser {
# Read-only fetch
}
mutation changeUser {
# Write then fetch
}
Anonymous queries have a shorthand.
{
# Read query
}
Using Drupal's REST, you would issue a GET request with a body as follows.
GET /node/1?_format=json HTTP/1.1
Host: drupaldevsite.dd:8083
Accept: application/json
Authorization: Basic YWRtaW46YWRtaWr=
Cache-Control: no-cache
And you would receive this response in JSON. But what if you only want the title?
{
"nid": [ { "value": "1" } ],
"uuid": [ { "value": "a8b233e0-ffff-434f-be00-ce1ac4599499" } ],
"vid": [ { "value": "1" } ],
"type": [ { "target_id": "page" } ],
"langcode": [ { "value": "en" } ],
"title": [ { "value": "Gang Jailed: Hoverboard Rampage Destroys Courthouse" } ],
// ...
}
Selection sets are subsets of fields on an object. They are made up of \n
-separated fields, which are discrete pieces of information.
{
entity {
node(id: 1) {
title
}
}
}
Selection sets are subsets of fields on an object. They are made up of \n
-separated fields, which are discrete pieces of information.
{
"entity": {
"node": {
"title": "Gang Jailed: Hoverboard Rampage Destroys Courthouse"
}
}
}
Fields can describe relationships with other data. Top-level fields are typically globally accessible.
{
entity {
node(id: 1) {
title
created
}
}
}
Fields can describe relationships with other data.
{
"entity": {
"node": {
"title": "Gang Jailed: Hoverboard Rampage Destroys Courthouse",
"created": "1460089883"
}
}
}
Fields return values and can carry arguments.
{
entity {
node(id: 1) {
title
created
}
}
}
Fields return values and can carry arguments.
{
"entity": {
"node": {
"title": "Gang Jailed: Hoverboard Rampage Destroys Courthouse",
"created": "1460089883"
}
}
}
More than one argument is possible in an arbitrary order.
{
entity {
node(id: 1) {
title
created
image(height: 72, width: 72)
}
}
}
Fields can be aliased.
{
content:entity {
node(id: 1) {
title
created
}
}
}
Fields can be aliased.
{
"content": {
"node": {
"title": "Gang Jailed: Hoverboard Rampage Destroys Courthouse",
"created": "1460089883"
}
}
}
Aliases are also useful for disambiguation.
{
content:entity {
node(id: 1) {
title
created
thumbnail:image(height: 72, width: 72)
picture:image(height: 250, width: 250)
}
}
}
Aliases are also useful for disambiguating identically named fields.
{
"content": {
"node": {
"title": "Gang Jailed: Hoverboard Rampage Destroys Courthouse",
"created": "1460089883",
"thumbnail": "http://i.usatoday.com/img/gang-jailed-hoverboard-rampage-72x72.jpg",
"picture": "http://i.usatoday.com/img/gang-jailed-hoverboard-rampage-250x250.jpg"
}
}
}
Fragments allow for reusable fields. Fields in fragments are added at the same level of invocation as adjacent fields.
{
entity {
node(id: 1) {
title
...customFields
}
}
}
fragment customFields on EntityNodeArticle {
fieldAuthor
fieldTags {
name
}
}
Fragments allow for reusable fields. Fields in fragments are added at the same level of invocation as adjacent fields.
{
"entity": {
"node": {
"title": "Gang Jailed: Hoverboard Rampage Desetroys Courthouse",
"fieldAuthor": "Compu-Fax",
"fieldTags": {
"name": "Hill Valley"
}
}
}
}
Fragments declare types, which facilitate conditionally applied fields. Fragments can also be arbitrarily nested.
{
entity {
node(id: 1) {
title
...customFields
}
}
}
fragment customFields on EntityNodeArticle {
author:fieldAuthor {
entity {
references {
node {
title
...customAuthorFields
}
}
}
}
}
fragment customAuthorFields on EntityNodeAuthor {
location:fieldLocation
organization:fieldOrganization
}
Fragments declare types, which facilitate conditionally applied fields. Fragments can also be arbitrarily nested.
{
"entity": {
"node": {
"title": "Gang Jailed: Hoverboard Rampage Desetroys Courthouse",
"author": {
"entity": {
"references": {
"node": {
"title": "Compu-Fax",
"location": "Hill Valley, CA",
"organization": "USA Today"
}
}
}
}
}
}
}
Inline fragments can improve code legibility and be anonymous.
{
entity {
node(id: 1) {
title
... on EntityNodeArticle {
author:fieldAuthor {
entity {
references {
node {
title
... on EntityNodeAuthor {
location:fieldLocation
organization:fieldOrganization
}
}
}
}
}
}
}
}
}
GET
is required.Variables are defined at the top of and scoped to operations.
query getArticle($nid: Int) {
node(id: $nid) {
title
... on EntityNodeArticle {
body
}
}
}
Directives alter execution behavior and can be used to conditionally include (@include
) or exclude (@skip
) fields.
query getArticle($published: Boolean, $nid: Int) {
node(id: $nid) {
title
... @include(if: $published) {
body
}
}
}
Directives alter execution behavior and can be used to conditionally include or exclude fields.
// $published resolves to false
{
"article": {
"title": "Gang Jailed: Hoverboard Rampage Destroys Courthouse"
}
}
// $published resolves to true
{
"article": {
"title": "Gang Jailed: Hoverboard Rampage Destroys Courthouse",
"body": "Reckless hoverboarders careened into Hill Valley's City Courthouse late yesterday afternoon, causing serious damage to the structure.",
}
}
The GraphQL spec recommends supporting @skip
and @include
.
query getArticle($published: Boolean, $authorIsAnonymous: Boolean) {
article:node(id: 992) {
title
author @skip(if: $authorIsAnonymous)
... @include(if: $published) {
body
image(width: 960)
}
}
}
In both queries and mutations, all fields apart from top-level mutation fields resolve idempotently.
# Pseudocode.
mutation favoriteArticle {
favoriteArticle(id: 992) {
node {
favoriteCount
}
}
}
In both queries and mutations, all fields apart from top-level mutation fields resolve idempotently.
// Server performs favoriteArticle write.
{
"node": {
"favoriteCount": 1022
}
}
Mutations will not be available in the initial release of the GraphQL module. This example mutation supplies a new title to a particular node before fetching the new title.
# Pseudocode
mutation updateTitle {
setTitle(title: "Gang Jailed: Hoverboard Rampage Destroys Courthouse") {
node(id: 992) {
title
}
}
}
Mutations will not be available in the initial release of the GraphQL module. This example mutation supplies a new title to a particular node before fetching the new title.
// Before setTitle mutation
{
"node": {
"title": "Youth Jailed: Marty McFly Junior Arrested for Theft"
}
}
// After setTitle mutation
{
"node": {
"title": "Gang Jailed: Hoverboard Rampage Destroys Courthouse"
}
}
Here is an example of what an update of a node might look like in lieu of issuing a POST
request.
# Pseudocode
mutation {
updateNode(id: 123, values: {
title: "Gang Jailed: Hoverboard Rampage Destroys More than Just Courthouse",
body: "Breaking: More portions of downtown Hill Valley were damaged today in the recent rampage.",
fieldTags: "Breaking news"
}) {
title
}
}
In GraphQL, schemas define the capabilities of your GraphQL server — which types and directives it supports — and validate your queries.
A GraphQL server should generally support these scalar types at minimum.
Int
Float
String
Boolean
ID
(serialized as String
)Types are objects that are validated against and represent the full breadth of selectable fields.
type EntityNodeArticle {
title: String
body: String
}
Types can refer to other types you have created or recursively to itself.
type EntityNodeArticle {
title: String
body: String
related: EntityNodeArticle
}
If a type object is nested within another, some field within the nested object must be selected. Therefore this query is valid.
{
title
related {
title
}
}
But this one is invalid.
{
title
related
}
Field arguments are defined by expressing all possible arguments and expected input types.
type EntityNodeArticle {
title: String
body: String
image(height: Int, width: Int): String
}
Once this type object is defined, these queries are valid.
{
entity {
node {
image(height: 100)
}
}
}
# New query
{
entity {
node {
image(height: 100, width: 100)
}
}
}
EntityNode
is an interface of the underlying Node
object: If you have only one bundle, then the interface name becomes EntityNodeNode
.Interfaces are implemented by GraphQL objects in case a field is present on sibling types.
interface EntityNode {
title: String
}
type EntityNodeArticle implements EntityNode {
body: String
}
Types invoked within type objects can refer to interfaces.
interface EntityNode {
title: String
}
type EntityNodeArticle implements EntityNode {
body: String
fieldAuthor: EntityNodeAuthor
}
type EntityNodeAuthor implements EntityNode {
fieldLocation: String
fieldOrganization: String
}
This allows for the fragment examples we saw earlier.
{
entity {
node(id: 1) {
title
... on EntityNodeArticle {
body
}
}
}
}
Unions in GraphQL are objects that could be one of multiple types, such as a search result.
# Pseudocode
Union Result = Node | TaxonomyTerm
type Node {
title: String
body: String
}
type TaxonomyTerm {
title: String
vocabulary: Vocabulary
}
type Search {
response: Result
}
This allows for us to set type-based conditions in our queries, since Result
can be either type.
# Pseudocode
query searchQuery {
response {
# The following field is only valid if all Result types have title.
title
... on Node {
body
}
... on TaxonomyTerm {
vocabulary
}
}
}
GraphQL allows for exhaustive introspection of the schema. For example, take EntityNodeArticle
.
interface EntityNode {
title: String
}
type EntityNodeArticle implements EntityNode {
body: String
}
We can build a query selecting the built-in field __type
.
{
__type(name: "EntityNodeArticle") {
name
fields {
name
type {
name
}
}
}
}
This returns a hierarchy of the type object.
{
"__type": {
"name": "EntityNodeArticle",
"fields": [
{
"name": "title",
"type": { "name": "String" }
},
{
"name": "body",
"type": { "name": "String" }
},
]
}
}
You can also use the __schema
field to access default types.
{
__schema {
types {
name
}
}
}
Sebastian Siemssen (fubhy) was featured in the Barcelona Driesnote and is completing a GraphQL implementation on Drupal using the TypedData API.
GraphiQL is a GUI for testing and debugging queries.