Graphql defer example
January 22, 2021
Graphql comes with handy capabilities when developing end user facing applications and in this blog post I’m going to talk about @defer directive and demonstrate it with a working example in NodeJS.
If you want to follow along with me checkout my example github repo graphql-defer-example
Both @defer and @stream directives are still under proposal stage as of this writing and with Apollo its available as an experimental feature.
With standard graphql request/reponse model, if you are requesting large amount of data from your graphql query, the client has to wait until all the fields are resolved in graphql server to get a response.
With the @defer directive you can de-prioritize part of your data in your query so that they’ll be available to the client at a later stage.
To achieve this we can use standard htt multipart responses from our graphql server.
In this example, I’ll be using
expressas the server.graphql-helixto process and execute the graphql server request. Checkout this awesome post about graphql-helixnexusto create the typed schemagraphql@experimental-stream-defergraphql experimental published package.
Refer to graphql-defer-example repo if you want to examine the complete code.
1. Creating an express server with /graphql endpoint
import express from 'express'
const app = express()
const PORT = 8000
app.use(express.json())
app.use('/graphql', async (req, res) => {
return res.send('ok')
})
app.listen(PORT, () => {
console.log(`⚡️[server]: Server is running at https://localhost:${PORT}`)
})2. Setup the typed graphql schema with nexus
import { makeSchema, queryType, objectType, list } from 'nexus'
import * as path from 'path'
import { products } from './resolvers'
...
const Query = queryType({
definition(t) {
t.field('product', {
type: Product,
resolve: () => ({id: 'x', name: 'y', images: []}) // return static since we will focus more on products query
})
t.field('products', {
type: list(Product),
resolve: products
})
},
})
const Image = objectType({
name: 'Image',
definition(t) {
t.string('url')
}
})
const Product = objectType({
name: 'Product',
description: 'Defines a single product',
definition(t) {
t.nonNull.string('id')
t.string('name')
t.field('images', {
type: list(Image)
})
t.field('relatedProducts', {
type: list(Product),
})
}
})
export const schema = makeSchema({
types: [Query, Product, Image],
outputs: {
schema: path.join(__dirname, '../generated/schema.graphql'),
typegen: path.join(__dirname, '../generated/typings.ts'),
},
})Here we have the products resolver that will be resolved to a static data included in the repository.
3. Mimick the slow resolver so we can experiment @defer directive
...
const Product = objectType({
name: 'Product',
description: 'Defines a single product',
definition(t) {
t.nonNull.string('id')
t.string('name')
t.field('images', {
type: list(Image)
})
t.field('relatedProducts', {
type: list(Product),
})
t.field('comments', {
type: list('String'),
resolve: async ({ id }) => new Promise((resolve) => {
const productComments = comments(id) || null
setTimeout(() => resolve(productComments), 1000) // settimeout for testing deferring on comments
})
})
}
})
...Notice the comments field that will be resolved after 1 second when we request it with our products.
4. Add graphql-helix to process the incoming graphql queries
import express from 'express'
import {
getGraphQLParameters,
processRequest,
shouldRenderGraphiQL,
renderGraphiQL
} from 'graphql-helix'
import { schema } from './graphql/schema'
....
app.use('/graphql', async (req, res) => {
const {
query,
variables,
operationName
} = getGraphQLParameters(req)
const result = await processRequest({
schema,
query,
variables,
operationName,
request: req,
})
})
...The result you’ll get from graphql-helix has 3 types.
- RESPONSE - when you have a query without
@defer, @streamdirectives - PUSH - when you have a graphql subscription
- MULTIPART_RESPONSE - when your query includes
@defer or @streamdirective
Since we are interested in MULTIPART_RESPONSE lets process it.
5. Process and send a response with a multipart response
...
if(result.type === 'MULTIPART_RESPONSE') {
req.on('close', () => {
result.unsubscribe()
})
const response = new MultipartResponse(res, 'mixed', 'gcgfr')
await result.subscribe((res) => {
response.add(res)
})
return response.end()
}
...Since there are no proper multipart response builder available, for this example I created a basic MultipartResponse class for express.
The important things to notice are that we use express response.writeHead initially and then response.write methods for subsequent responses that we will be sent to client as patches. And finally, response.end at the end to indicate to close the connection.
6. Lets add graphiql to test out our defer example
graphql-helix comes with nice and easy function to mount graphiql based the browser request.
...
app.use('/graphql', async (req, res) => {
...
if(shouldRenderGraphiQL(req)) {
return res.send(renderGraphiQL({defaultQuery: exampleQuery}))
}
...
})
...defaultQuery is the one that get populated on your graphiql interface as the default.
7. Try out a query with defer directive in graphiql interface
fragment Comments on Product{
comments
}
fragment RelatedProducts on Product {
relatedProducts {
id
name
}
}
query {
products{
id
...Comments @defer
...RelatedProducts @defer
}
}Here we define two fragments Comments, RelatedProducts so we can apply @defer directive to them.
8. Lets examine the response we get
--gcgfr
Content-Type: application/json; charset=utf-8
Content-Length: 71
{"data":{"products":[{"id":"1"},{"id":"2"},{"id":"3"}]},"hasNext":true}
--gcgfr
Content-Type: application/json; charset=utf-8
Content-Length: 127
{"data":{"relatedProducts":[{"id":"2","name":"product-2"},{"id":"3","name":"product-3"}]},"path":["products",0],"hasNext":true}
--gcgfr
Content-Type: application/json; charset=utf-8
Content-Length: 97
{"data":{"relatedProducts":[{"id":"1","name":"product-1"}]},"path":["products",1],"hasNext":true}
--gcgfr
Content-Type: application/json; charset=utf-8
Content-Length: 97
{"data":{"relatedProducts":[{"id":"1","name":"product-1"}]},"path":["products",2],"hasNext":true}
--gcgfr
Content-Type: application/json; charset=utf-8
Content-Length: 91
{"data":{"comments":["good review 1","bad review 1"]},"path":["products",0],"hasNext":true}
--gcgfr
Content-Type: application/json; charset=utf-8
Content-Length: 90
{"data":{"comments":["average review 1","review 1"]},"path":["products",1],"hasNext":true}
--gcgfr
Content-Type: application/json; charset=utf-8
Content-Length: 80
{"data":{"comments":["average review 2"]},"path":["products",2],"hasNext":false}We see 7 parts in our reponse each seperated with the boundary --gcgfr each which arrived as a chunk to the client seperately.
Also in the graphiql interface its visible thats the comments are appearing after few seconds.

Thank you for reading.