Building a Relay-compatible GraphQL Server

by Nikolaus Piccolotto - 3 Feb 2017

You’ve probably heard about GraphQL and Relay, and how they will change everything we know about data management in our applications. At Zalando, we’re open to experimenting with and adding to our technology stack, and this is no exception. In this article I’d like to shed some light on Relay’s internals and build a GraphQL server on top of the existing Zalando REST API, making it compatible with Relay. The application will consist of a list of articles and an article detail page, featuring recommendations. It is assumed that readers will be familiar with ES6, React, and have some cursory knowledge of GraphQL.

A GraphQL refresher

GraphQL is a query language developed by Facebook (GraphQL Introduction, GraphQL: A data query language). Though general usage is possible, it was designed with the needs of UIs in mind and is considered more efficient than REST APIs for this purpose. If you ever thought that /recommendations?articleIds=1,2,4&include=name,price is not a very RESTful endpoint, you might want to consider a BFF with GraphQL.

One of the major differences compared to a REST API is the necessity to define a schema for your data and queries. While you can do that for REST APIs as well (e.g. with OpenAPI), you can’t ask for specific data: You would have to implement the include parameter from the above example yourself. Also, the server may or may not return data according to the defined schema. This is in stark contrast to GraphQL, where you get exactly what you asked for.

To query an article in our GraphQL server you might submit a query like this:

query {
Article(id: "PU142E04G-Q11") {
name
}
}

And get a response like this:

{
"data": {
"Article": {
"name": "UB - Tracksuit bottoms - black"
}
}
}

Note how the response structure mirrors your query. Also, you can’t query for “everything” or something that is not contained in the schema:

query {
Article(id: "some-article-id")
}
{
"errors": [
{
"message": "Field \"Article\" of type \"Article\" must have a selection of subfields. Did you mean \"Article { ... }\"?",
"locations": [
{
"line": 2,
"column": 3
}
]
}
]
}
query {
Article(id: "some-article-id") {
isPretty
}
}
{
"errors": [
{
"message": "Cannot query field \"isPretty\" on type \"Article\".",
"locations": [
{
"line": 3,
"column": 5
}
]
}
]
}

With that in mind, let’s first build the schema for a non-Relay GraphQL server, because having GraphQL does not automatically mean you can use Relay with it. We’ll implement the necessary changes afterwards.

The schema

Our data is pretty straightforward as it only deals with articles. We have an article type consisting of a name, preview image (thumbnail), brand information, a list of proper images, and recommendations, which are a list of articles. Note that fields without an exclamation mark are nullable, so we can use the same data type for preview and detail purposes.

type Brand {
name: String
logoUrl: String
}
enum Gender {
MALE
FEMALE
}
type Image {
thumbnailUrl: String
smallUrl: String
mediumUrl: String
largeUrl: String
}
type Article {
id: ID! # non-nullable, is guaranteed to exist on every article
name: String
thumbnailUrl: String
brand: Brand
genders: [Gender]
images: [Image]
recommendations: [Article]
}
type Query {
Article(id: ID!): Article
Articles: [Articles]
}

The Query type is a special GraphQL type as it defines entry points to the GraphQL API. You can only query for fields of the Query type. Here we have defined queries for a list of articles and a specific article.

The first GraphQL server implementation

Since our GraphQL server will at first only proxy calls to the Zalando REST API, we can take the schema description from above and generate a JavaScript representation from it. It’s very convenient — see below for all of the server code (excluding API calls):

const express = require('express'),
fs = require('fs'),
bodyParser = require('body-parser'),
// we use graphql for query execution
{graphql, buildSchema} = require('graphql'),
// our article schema
Schema = String(fs.readFileSync('./data/schema.graphql')),
app = express(),
// functions to fetch data from REST API and transform according to schema
api = require('./api'),
jsSchema = buildSchema(Schema);
// this defines the entry point to the GraphQL API
// the GraphQL query executor will start resolving fields from here
// (how this works exactly is explained later)
const queryResolver = {
Article: ({id}) => api.fetchArticle(id),
Articles: () => api.fetchArticles()
};
// GraphQL queries will be plain text
app.use(bodyParser.text());
// we use POST /graphql to submit queries
app.post('/graphql', (req, res) => {
const query = req.body;
graphql(jsSchema, query, queryResolver)
.then(result => res.status(200)
.json(result))
});
app.listen(process.env.PORT || 3001);

And we can verify it works by executing:

curl -X POST -H "Content-Type: text/plain" -d "{ Articles { name } }" http://localhost:3001/graphql -v

That was quick! If you just want to wrap your REST API in GraphQL, this is already enough. But let’s go a step further and take a look at Relay.

Relay introduction

Relay is a client framework designed to work with a compatible GraphQL server. It makes assumptions on which types exist, how types are named, and which filters are available. This means that it won’t work out of the box with any GraphQL server. Essentially, you tie React components to “fragments” of GraphQL types, specifying fields that you expect. Relay takes this data dependency tree, generates the necessary GraphQL query (note the singular!), runs it, and distributes the data back to React components.

What kind of problems does it solve? (Which other problems it introduces we’ll see later — everything is a tradeoff.)

Firstly, the network access is more efficient with Relay compared to working with REST APIs. Relay collects all data requirements and sends a single query to the server. (I’m not sure if it’s the case already, but it could also generate the minimum necessary query, e.g. when you have multiple fragments with overlapping fields on the same resource.) It also caches data already fetched automatically and won’t refetch if it finds something in the cache.

Second, your queries are located next to your component. Suppose you want to use another field of a type. Without Relay, you now need to touch the data fetching code (probably Redux actions) and possibly one or many parent components that distribute this data. Changing some props on the parent component might break child components. With Relay, you add the field to the fragment and can use it directly — that’s it.

Updating our GraphQL server

Relay wants to do three things:

  1. Identify objects
  2. Navigate large lists
  3. Do mutations (not covered as our application is read-only)

Identifying objects
Relay wants a single way to query for an object. What’s required is an interface Node with a single field id of type ID and a query for that, called node. Relay assumes you have globally unique IDs. If you don’t, you can make them so by concatenating type name with id, e.g. article-42. Since we only deal with a single object type (Article), we can reuse the existing ID.

The following changes are necessary to our schema:

interface Node {
id: ID!
}
type Article implements Node {
id: ID!
name: String
thumbnailUrl: String
brand: Brand
genders: [Gender]
images: [Image]
recommendations: [Article]
}
type Query {
Articles: [Article]
Article(id: ID!): Article
node(id: ID!): Node
}

Since the signature and semantics of our Article and node queries are the same, we can use the same code too!

const queryResolver = {
Article: ({id}) => api.fetchArticle(id),
Articles: () => api.fetchArticles(),
node: ({id}) => api.fetchArticle(id)
};

Let’s run a query to test it:

query {
node(id: "PU142E04G-Q11") {
id
}
}
{
"data": {
"node": null
},
"errors": [
{
"message": "Generated Schema cannot use Interface or Union types for execution.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"node"
]
}
]
}

What happened here? This is related to how GraphQL fragments are supposed to work. Suppose we want to query for an article via the node query and fetch the name. Since an article is also a node, this should be possible. However, we can’t just add the name field to the query, because it returns a Node and this (interface) type only has an id. What we can do instead is this:

query {
node(id: "PU142E04G-Q11") {
id
... on Article {
name
}
}
}

We say “if the node returned is of type Article, then take the name from it”. That’s why an interface type has to be mapped to an object type at runtime. Unfortunately, there is no way to my knowledge that we can achieve this by modifying the schema (string) definition — we have to rewrite our JS schema representation by hand. We can then implement isTypeOf on the Article type:

const Article = new GraphQLObjectType({
name: 'Article',
interfaces: [Node],
// whatever has an id is considered an article (we don’t have anything else)
isTypeOf: (value) => !!value.id,
fields: {
id: {
type: new GraphQLNonNull(GraphQLID)
},
name: {
type: GraphQLString
},
thumbnailUrl: {
type: GraphQLString
},
brand: {
type: Brand
},
genders: {
type: new GraphQLList(Gender)
},
images: {
type: new GraphQLList(Image)
},
recommendations: {
type: () => new GraphQLList(Article)
}
}

As you can see, our server stays more or less the same, we just exchange the buildSchema step with the JS schema we just built.

Navigating large lists

Next up, Relay wants a single way to essentially do pagination. Usually, you have two types of one-to-many relations in your data: Those with a limited amount of items, like our article images (we don’t know exactly how many there are, but usually only a few), and those with unlimited items, like our recommendations (they will get worse the more we fetch, but we can always get more). For the former you will use List types, whereas Relay has a special type for the latter: Connections.

A Connection type in Relay ends with “Connection”, so our recommendations will be of type ArticleConnection. It holds two fields: Edges and PageInfo. Edge is a sort of intermediate type between the Connection and the type you’re connecting to, holding a cursor (the id of a node in the simplest case) and the node. Its name needs to end in “Edge”. Cursors are used to… well, do cursor-based pagination, as opposed to offset/page-based pagination. The PageInfo type should be straightforward.

We’ll change our schema like this:

type ArticleConnection {
pageInfo: PageInfo
edges: [ArticleEdge]
}
type ArticleEdge {
node: Article!
cursor: ID!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
}
type Article implements Node {
id: ID!
name: String
thumbnailUrl: String
brand: Brand
genders: [Gender]
images: [Image]
recommendations(first: Int, last: Int, before: ID, after: ID): ArticleConnection
}

The filters on the recommendations field of the Article are also a requirement by Relay, as it otherwise doesn’t have a way to formulate its pagination queries (“give me 10 items after the one with ID foo”).

Before implementing the desired behavior, let’s think about how we’ll manage recommendations. It’s a separate API call, so we don’t want to execute it every time an article is requested. And relatedly, since recommendations are also articles, we have to make sure not to find ourselves in an infinite loop of fetching recommendations.

Luckily, there is already a way to achieve this in GraphQL. The way a query works in a nutshell is that for every object returned, it will call a resolver function for requested attributes, and repeat the process on the returned objects until only scalar types (Int, String…) are left. The default resolver is a simple lookup (obj[‘attribute’]), but we can override it! If an attribute is not requested, its resolver function will not be called, thus it’s a perfect fit for our recommendations.

const Article = new GraphQLObjectType({
name: 'Article',
interfaces: [Node],
isTypeOf: (value) => !!value.id,
fields: {
id: {
type: new GraphQLNonNull(GraphQLID)
},
name: {
type: GraphQLString
},
thumbnailUrl: {
type: GraphQLString
},
brand: {
type: Brand
},
genders: {
type: new GraphQLList(Gender)
},
images: {
type: new GraphQLList(Image)
},
recommendations: {
type: ArticleConnection,
args: {
first: {
type: GraphQLInt
},
last: {
type: GraphQLInt
},
before: {
type: GraphQLID
},
after: {
type: GraphQLID
}
},
resolve: function (article, params) {
// toConnection is a helper function to return a Connection type compatible structure from an API call
return toConnection(api.fetchRecommendations(article.id), params)
}
}
}
});

Let’s try it out!

query {
node(id: "PU142E04G-Q11") {
id
... on Article {
name
recommendations(first: 1) {
edges { node { id name } }
}
}
}
}
{
"data": {
"node": {
"id": "PU142E04G-Q11",
"name": "UB - Tracksuit bottoms - black",
"recommendations": {
"edges": [
{
"node": {
"id": "AD542E0FX-C11",
"name": "Tracksuit bottoms - medium grey heather/black"
}
}
]
}
}
}
}

Note that there is a helper library available for these modifications, but it’s better to understand something first before using an abstraction.

The Relay application

Now that we’re ready, let’s build a client that talks to our new Relay-compatible API. We will have a list of articles:

And an article detail view that leverages the article list for displaying recommendations:

First we’ll build the article list, as we’ll need that component from the outset.

ArticleList

ArticleList is a pretty simple component, as it only needs some articles and a flag for whether or not there are more articles available. There is no point in showing the “load more” button without the aforementioned flag. This is how it looks without Relay:

class ArticleList extends Component {
render() {
const {articles, hasNext} = this.props;
return <main>
<section className="article-list">
{articles.edges.map((a, i) => <ArticlePreview key={i}
article={a.node}/>)}
{hasNext ?
<div
className="btn-load-more"
onClick={() =>
this.props.onLoadMore()}>
<div className="btn-load-more--plus"></div>
<div className="btn-load-more--text">
Load more articles
</div>
</div>
:
null}
</section>
</main>
}
}

With Relay, we need to create a container along with the appropriate fragment, since our articles and hasNext are returned by the backend. Both fields are contained in an ArticleConnection, so we write a fragment for that type.

export default Relay.createContainer(ArticleList, {
fragments: {
articles: (vars) => Relay.QL`
fragment on ArticleConnection {
pageInfo { hasNextPage }
edges {
node {
# defensive programming in case we pass Relay variables around
# we need to pull in the ArticlePreview fragment, because we render article previews
${ArticlePreview.getFragment('article', {...vars})}
}
}
}`
}
})

(Note: You might wonder what Relay.QL is exactly. It will be transformed by the babel-relay-plugin at build time to an abstract syntax tree representing the GraphQL query. That’s the reason why you need to export your schema to a JSON file, so that Relay can do client-side validation of the query.)

If everything works we can be sure to have the property articles in our component which contains the data of the fragment (pageInfo.hasNextPage and edges.node). The Relay container will throw an error otherwise. Of course, we have to make minor changes on the React component itself to accommodate new props.

We’ll build the other components with the same logic.

ArticlePreview

An article preview consists of only a thumbnail, name, and brand name (and id for navigation purposes).

class ArticlePreview extends Component {
render() {
return <div className="article"
data-id={this.props.article.id}
onClick={() => this.props.onClick && this.props.onClick(this.props.article.id)}>
<img
className="article--image"
src={this.props.article.thumbnailUrl}/>
<div className="article--brand">
<strong>{this.props.article.brand.name}</strong>
</div>
<div className="article--name">
<small>{this.props.article.name}</small>
</div>
</div>
}
}
export default Relay.createContainer(ArticlePreview, {
fragments: {
article: () => Relay.QL`
fragment on Article {
id
name
thumbnailUrl
brand {name}
}`
}
})

ArticleDetail

The detail view features more images at a bigger size as well as recommendations for an article. We will initially show the first five recommended articles and then show the next five every time the button is clicked. The way this works is we increment the pageSize variable in the Relay query each time with relay.setVariables. (When you wrap a React component in a Relay component, relay is automatically set on the props — you then have some useful methods available.)

class ArticleDetail extends Component {
onLoadMore() {
this.props.relay.setVariables({
pageSize: this.props.relay.variables.pageSize + 5
})
}
render() {
const {article} = this.props;
return <div className="article-detail">
<h3 className="article-detail--brand">
<img src={article.brand.logoUrl} alt={article.brand.name}/> {article.brand.name}
</h3>
<h1 className="article-detail--name">
{article.name}
</h1>
<ImageSlider images={article.images.map(img => img.largeUrl)}/>
<ArticleList articles={article.recommendations}
onLoadMore={this.onLoadMore.bind(this)}
onNavigate={this.props.onNavigate}/>
</div>
}
}
export default Relay.createContainer(ArticleDetail, {
initialVariables: {
pageSize: 5
},
fragments: {
article: () => Relay.QL`
fragment on Article {
name
thumbnailUrl
brand {name logoUrl}
images {largeUrl}
recommendations(first: $pageSize) { ${ArticleList.getFragment('articles')} }
}`
}
});

GraphQL server

Previously, we defined our server endpoint to expect a POST request with text/plain body on /graphql. This works perfectly fine, however Relay will submit an application/json body with query and variables fields, so we have to change our server accordingly.

app.use(bodyParser.json());
app.post('/graphql', (req, res) => {
let query = '',
variables = {};
if (typeof req.body === 'string') {
query = req.body
} else if (typeof req.body === 'object') {
query = req.body.query
variables = req.body.variables
}
try {
graphql(Schema, query, {}, {}, variables)
.then(result => res.status(200)
.json(result))
.catch(e => {
throw e;
})
} catch (e) {
console.log(e)
res.status(500)
.send(e.message)
}
});

By treating it this way, it will accept both queries sent with curl or a REST client and everything that comes from Relay. Let’s tie it all together in the next step.

App

We created our primitives in the previous steps, but how will we switch between list and detail views? How do we fetch the initial list of articles? We have only defined fragments at this point, so where are all the objects coming from that they are supposed to work on?

To answer all of these questions, let’s first try to persuade Relay to render *anything*. The first approach could be like so:

// in case your Relay server does not run on the same host
Relay.injectNetworkLayer(
new Relay.DefaultNetworkLayer('http://localhost:3001/graphql', {
credentials: 'cors',
})
);
DOM.render(
<Relay.RootContainer Component={ArticleList}
route={{
queries: {
articles: () => Relay.QL`query {Articles(first: $pageSize)}`
},
params: {
pageSize: 5
},
name: 'ArticlesQuery'
}} />,
document.getElementById('app'));

This does not look like much, but a lot of things happen under the hood: We define a query skeleton articles. Relay will look for a fragment articles in the container passed as Component and automatically insert it into the query sent to the GraphQL server. If the query succeeds, it will pass the data as the property articles to ArticleList and render it. In case the query fails, it will retry three times and then ultimately fail. (You can configure the amount of retries, timeouts, and what to render on the RootContainer.) If the data requested is already in the local Relay cache, it will take it from there and not query the server. In any case, it seems to work:

You might not immediately notice, but the “load more” button is missing because the component does not know what to do if it were clicked (onLoadMore property is missing). How do we fix this? Relay’s RootComponent does not pass unknown properties down to ArticleList. We could probably create a Higher-Order Component that returns an ArticleList with onLoadMore property, but what should the function execute? We would have to change the page size and trigger a re-render ourselves, which doesn’t sound like fun. We also can’t change the query parameters from inside the ArticleList components (Relay variables are only valid locally). Can we make Relay take care of all this? In the end, the whole point of it is to be in charge of data fetching and re-rendering as necessary.

We can make this happen by creating an intermediate container, our actual application. It will know when to display an ArticleList or ArticleDetail and change Relay variables accordingly. To make fragment handling easier for the container, we will introduce an additional query on our server that can return a single article or a list of articles.

type Query {
Articles(first: Int, last: Int, before: ID, after: ID): ArticleConnection
Article(id: ID!): Article
Viewer: Viewer
node(id: ID!): Node
}
type Viewer {
article(id: ID!): Article
articles(first: Int, last: Int, before: ID, after: ID): ArticleConnection
}

The application can then work on fragments of the Viewer type.

class App extends React.Component {
onShowList() {
this.props.relay.setVariables({
showDetailPage: false
})
}
onLoadMore() {
this.props.relay.setVariables({
pageSize: this.props.relay.variables.pageSize + 10,
showDetailPage: false
})
}
onNavigate(id, updateHistory = true) {
this.props.relay.setVariables({
articleId: id,
showDetailPage: true
})
}
render() {
return this.props.relay.variables.showDetailPage ?
<ArticleDetail article={this.props.Viewer.article} /> :
<ArticleList articles={this.props.Viewer.articles}
onLoadMore={this.onLoadMore.bind(this)} />
}
}
export default Relay.createContainer(App, {
initialVariables: {
articleId: window.location.pathname.substr(1),
showDetailPage: window.location.pathname.substr(1) !== '',
pageSize: 20
},
fragments: {
Viewer: () => Relay.QL`
fragment on Viewer {
articles(first: $pageSize) @skip(if: $showDetailPage) {
${ArticleList.getFragment('articles')}
}
article(id: $articleId) @include(if: $showDetailPage) {
${ArticleDetail.getFragment('article')}
}
}
`
}
});

The @skip and @include annotations are specific to Relay. As you probably guessed, depending on the annotation and passed boolean value, they may or may not include a fragment in the GraphQL query. For our application this is important, as we don’t have an articleId when we start on the list. The GraphQL server would return an error.

I glossed over a couple of things, most notably the routing code, but you can take a look at the whole project here.

Wrapping up

This was the first time I created a Relay application. I’m used to and usually work with the React+Redux combination, so a few differences stood out for me.

First of all, I really like the declarative data fetching. Do you want to use some more fields of an object? Just add it to the fragment and go ahead. Everything else is taken care of.

I also like that Relay is smart about fetching data. If I want to have twenty articles after previously having fetched ten, it already knows to only query for the next ten. The same goes for revisiting detail pages, it just takes what it has in the cache and doesn’t go to the network at all.

State management was not as easy to grasp. This is usually handled with Redux actions and stores. In this simple case, it may as well have been a local state of the App component. However, since GraphQL queries have to be generated at build time, they cannot access component state or properties, so we ended up inserting state management into Relay (showDetailPage — I feel that Relay shouldn’t care about this). This is where react-router-relay comes into play, as it helps when rendering different RootComponents. It would likely would have helped with my next problem.

A loading indicator (“spinner”) is only shown at application start, because that’s when the RootComponent is rendered. When switching between ArticleDetails, we don’t have this visual feedback. I initially thought that we could mitigate this by checking relay.pendingVariables, but it was always null.

To wrap things up, when should you use Relay? As always, it depends. There are a couple of companies using it already, however except for Facebook and Twitter, they’re not currently on my radar. Relay is a very new technology, which means you won’t find many resources to learn from compared to what we’ve already figured out. Having efficient network access and caching out of the box is quite nice, but there is an upfront cost (in terms of time) you pay for new technology. Colocated queries and components are also awesome, but when you lack a Facebook-sized team working on the same API, it might not be worth it. I’d say that Relay is worth investigating if you can tick a couple of these boxes:

  • Small, independent service/application
  • Desire to dive into uncharted territory
  • Many of your UI components work on subsets of data from the same object, like our ArticleDetail and ArticlePreview
  • Expensive queries on the backend, so that the client making fewer requests pays off there too
  • Objects exposed by API have many fields, but your client only needs a couple of them (not Relay-specific)
  • Facebook uses Relay for their mobile page, so it might be worth considering for that specific format

And as always, do whatever floats your boat. Send any questions you might have my way via Twitter at @prayerslayer.

Similar blog posts