GraphQL Basics
Setup
Install it via npm using yarn or npm:
yarn add graphql
Simple GraphQL schema example
const {graphql, buildSchema} = require('graphql');
/* Describes what's possible and what's not possible */const schema = buildSchema(` type Query { foo: String } type Schema { query: Query }`);
const resolvers = { foo: () => 'bar';};
const query = `{foo}`;/** * This is how we fire a request and get the data from GraphQL * graphql( * schema: GraphQLSchema, * requestString: string, * rootValue?: ?any, passed as root value to the executor * contextValue?: ?any, * variableValues?: ?{[key: string]: any}, * operationName?: ?string * ): Promise<GraphQLResult> */ graphql(schema, query, resolvers) .then((result) => console.log(`We got: ${result}`)) .catch((error) => console.log('There was an error'));
We can define different Query types like String, Int, ID, Boolean..., depends on what we need. We can mark a type to be NON-NULLABLE by putting an ! at the end.
Using List Type for collections
/* Extend our schema with a new type Video */const Schema = buildSchema(`type Video { id: ID, title: String, duration: Int, watched: Boolean}
type Query { video: Video videos: [Video]}`);
/* Some dummy data for 2 videos */const videoA = { id: "abc", title: "Create a GraphQL Schema", duration: 120, watched: true};const videoB = { id: "def", title: "Ember.js CLI", duration: 240, watched: false};
const videos = [videoA, videoB];
/* And we update our resolvers to resolve correct data */const resolvers = { video: () => videoA, videos: () => videos};
/* Now when we call graphQL with our new schema and resolvers we will get back a collection of videos */graphql(schema, query, resolvers) .then(result => console.log(`We got: ${result}`)) .catch(error => console.log("There was an error"));Using ExpressJS with GraphQL
Install express and express-graphql
yarn add express express-graphql
Use ExpressJS middleware graphqlHTTP
"use strict";
const express = require("express");const graphqlHTTP = require("express-graphql");const { graphql, buildSchema } = require("graphql");
const PORT = process.env.PORT || 3000;const server = express();
const schema = buildSchema(`type Video { id: ID, title: String, duration: Int, watched: Boolean}type Query { video: Video videos: [Video]}type Schema { query: Query}`);
const videoA = { id: "a", title: "Create a GraphQL Schema", duration: 120, watched: true};const videoB = { id: "b", title: "Ember.js CLI", duration: 240, watched: false};const videos = [videoA, videoB];
const resolvers = { video: () => ({ id: "1", title: "Foo", duration: 180, watched: true }), videos: () => videos};
server.use( "/graphql", graphqlHTTP({ schema, graphiql: true, rootValue: resolvers }));
server.listen(PORT, () => { console.log(`Listening on http://localhost:${PORT}`);});Now with this middleware we also get a tool called GraphiQL. If we start our server and then go to http://localhost:3000/graphql we will access the GraphiQL tool. Here we can make queries to GraphQL server and we can see the results.
Using JS to define our schema
GraphQL comes with some helpers. For example:
- GraphQLSchema
- GraphQLSchema
- GraphQLSchema
- GraphQLObjectType
- GraphQLID
- GraphQLString
- GraphQLInt
- GraphQLBoolean
Here is an example using the above helpers:
"use strict";
const express = require("express");const graphqlHTTP = require("express-graphql");const { GraphQLSchema, GraphQLObjectType, GraphQLID, GraphQLString, GraphQLInt, GraphQLBoolean} = require("graphql");
const PORT = process.env.PORT || 3000;const server = express();
const videoType = new GraphQLObjectType({ name: "Video", description: "A video on Egghead.io", fields: { id: { type: GraphQLID, description: "The id of the video." }, title: { type: GraphQLString, description: "The title of the video." }, duration: { type: GraphQLInt, description: "The duration of the video (in seconds)." }, watched: { type: GraphQLBoolean, description: "Whether or not the viewer has watched the video." } }});
const queryType = new GraphQLObjectType({ name: "QueryType", description: "The root query type.", fields: { video: { type: videoType, resolve: () => new Promise(resolve => { resolve({ id: "a", title: "GraphQL", duration: 180, watched: false }); }) } }});
const schema = new GraphQLSchema({ query: queryType});
const videoA = { id: "a", title: "Create a GraphQL Schema", duration: 120, watched: true};const videoB = { id: "b", title: "Ember.js CLI", duration: 240, watched: false};const videos = [videoA, videoB];
server.use( "/graphql", graphqlHTTP({ schema, graphiql: true }));
server.listen(PORT, () => { console.log(`Listening on http://localhost:${PORT}`);});Passing arguments in GraphQL query
Let's take a look at the previous example and let's add an argument ID to the query:
const queryType = new GraphQLObjectType({ name: 'QueryType', description: 'The root query type', fields: { video: { type: videoType, args: { id: { type: GraphQLID, description: 'The id of the video', } } resolve: (_, args) => { return getVideoById(args.id); } } }});The 'getVideoById' call will just return a Promise filtering all the videos and returning the one with the same ID passed as an argument.
Using GraphQLNonNull for required fields
Still looking at the example from before... we need to import this new method and use it where we define the type for the argument:
import GraphQLNonNull from 'graphql';
const queryType = new GraphQLObjectType({ name: 'QueryType', description: 'The root query type', fields: { video: { type: videoType, args: { id: { type: new GraphQLNonNull(GraphQLID), description: 'The id of the video', } } resolve: (_, args) => { return getVideoById(args.id); } } }});Now when we run the query, if the argument is not passed we will get an error and not a null value anymore.
Creating our first mutation
In the example below we will create a new mutation for adding additional videos...
// First we need our video typeconst videoType = new GraphQLObjectType({ name: "Video", description: "A video on Egghead.io", fields: { id: { type: GraphQLID, description: "The id of the video." }, title: { type: GraphQLString, description: "The title of the video." }, duration: { type: GraphQLInt, description: "The duration of the video (in seconds)." }, released: { type: GraphQLBoolean, description: "Whether or not the video has been released." } }});
// Then we define the mutation typeconst mutationType = new GraphQLObjectType({ name: "Mutation", description: "The root Mutation type.", fields: { createVideo: { type: videoType, args: { title: { type: new GraphQLNonNull(GraphQLString), description: "The title of the video." }, duration: { type: new GraphQLNonNull(GraphQLInt), description: "The duration of the video (in seconds)." }, released: { type: new GraphQLNonNull(GraphQLBoolean), description: "Whether or not the video is released." } }, resolve: (_, args) => { return createVideo(args); } } }});
// At the end we add the mutation to our schemaconst schema = new GraphQLSchema({ query: queryType, mutation: mutationType});Working with more complex mutations
When we have certain mutations that require more complex input parameters, we can leverage the Input Object Type in GraphQL using GraphQLInputObjectType.
// First we need to import the GraphQLInputObjectTypeimport { GraphQLInputObjectType } from "graphql";
// Then we define our new videoInputType objectconst videoInputType = new GraphQLInputObjectType({ name: "VideoInput", fields: { title: { type: new GraphQLNonNull(GraphQLString), description: "The title of the video." }, duration: { type: new GraphQLNonNull(GraphQLInt), description: "The duration of the video (in seconds)." }, released: { type: new GraphQLNonNull(GraphQLBoolean), description: "Whether or not the video is released." } }});
// At the end like before we are consuming it in the mutation type which will be then added to the schemaconst mutationType = new GraphQLObjectType({ name: "Mutation", description: "The root Mutation type.", fields: { createVideo: { type: videoType, args: { video: { type: new GraphQLNonNull(videoInputType) } }, resolve: (_, args) => { return createVideo(args.video); } } }});Using GraphQL interface
As we start building out more complex GraphQL schemas, certain fields start to repeat across different types. This is a perfect use-case for the Interface Type made available to us through GraphQL’s Type System.
// The interface file..."use strict";
const { GraphQLInterfaceType, GraphQLNonNull, GraphQLID } = require("graphql");const { videoType } = require("../");
const nodeInterface = new GraphQLInterfaceType({ name: "Node", fields: { id: { type: new GraphQLNonNull(GraphQLID) } }, resolveType: object => { if (object.title) { return videoType; }
return null; }});
module.exports = nodeInterface;// Let's consume our interface here...// First, of course, we need to import it...const nodeInterface = require("./src/node");
// Now we can use it in our VideoType definitionconst videoType = new GraphQLObjectType({ name: "Video", description: "A video on Egghead.io", fields: { id: { type: new GraphQLNonNull(GraphQLID), description: "The id of the video." }, title: { type: GraphQLString, description: "The title of the video." }, duration: { type: GraphQLInt, description: "The duration of the video (in seconds)." }, released: { type: GraphQLBoolean, description: "Whether or not the video has been released." } }, interfaces: [nodeInterface]});exports.videoType = videoType;Using Relay with GraphQL
We can get Relay by installing it from
npm install graphql-relayoryarn add graphql-relay
The GraphQL Relay Specification requires that a GraphQL Schema has some kind of mechanism for re-fetching an object. For typical Relay-compliant servers, this is going to be the Node Interface.
// Our node interface exampleconst { nodeDefinitions, fromGlobalId } = require("graphql-relay");const { getObjectById } = require("./data");
const { nodeInterface, nodeField } = nodeDefinitions( globalId => { const { type, id } = fromGlobalId(globalId);
return getObjectById(type.toLowerCase(), id); }, object => { const { videoType } = require("../");
if (object.title) { return videoType; }
return null; });
exports.nodeInterface = nodeInterface;exports.nodeField = nodeField;getObjectById is defined in the required data:
const getObjectById = (type, id) => { const types = { video: getVideoById };
return types[type](id);};We glue everything together:
// First the import...const { globalIdField } = require("graphql-relay");const { nodeInterface, nodeField } = require("./src/node");
// We define the GraphQL object type: videoTypeconst videoType = new GraphQLObjectType({ name: "Video", description: "A video on Egghead.io", fields: { id: globalIdField(), // *check this line* title: { type: GraphQLString, description: "The title of the video." }, duration: { type: GraphQLInt, description: "The duration of the video (in seconds)." }, released: { type: GraphQLBoolean, description: "Whether or not the video has been released." } }, interfaces: [nodeInterface]});exports.videoType = videoType;Using Relay Connection Type instead of GraphQL List Type
In order to properly traverse through collections, Relay-compliant servers require a mechanism to page through collections available in a GraphQL Schema.
// First we need to import what we need from Relayconst { globalIdField, connectionDefinitions, connectionFromPromisedArray, connectionArgs,} = require('graphql-relay');
// Now lets define the videoConnectionconst { connectionType: videoConnection } = connectionDefinitions({ nodeType: videoType,});
// Next we need to update the videos field, which now will have the type `videoConnection`const queryType = new GraphQLObjectType({ name: 'QueryType', description: 'The root query type.', fields: { node: nodeField, videos: { type: VideoConnection, args: connectionArgs, resolve: (_, args) => connectionFromPromisedArray( getVideos(), args, ), }, ...});To query the above we can simply execute the following query:
{ videos: { edges: { node { id, title, duration } } }}Add totalCount field
Lets add another field called totalCount, which should describe the count of all of the videos.
// Add it to the connectionDefinitionsconst { connectionType: VideoConnection } = connectionDefinitions({ nodeType: videoType, connectionFields: () => ({ totalCount: { type: GraphQLInt, description: "A count of the total number of objects in this connection.", resolve: conn => { return conn.edges.length; } } })});Now we can query this in the following way:
{ video(last: 1) { // we can use here also `first, after or before...` edge { node { title } } }}GraphQL mutations with Relay
In order to support mutations in Relay, there is a requirement that the GraphQL Server exposes mutation fields in a standardized way. This standard includes a way for mutations to accept and emit an identifier string, allowing Relay to track mutations and responses.
// First we need to import `mutationWithClientMutationId`const { mutationWithClientMutationId } = require("graphql-relay");
// Let's create `videoMutation`const videoMutation = mutationWithClientMutationId({ name: "AddVideo", inputFields: { title: { type: new GraphQLNonNull(GraphQLString), description: "The title of the video." }, duration: { type: new GraphQLNonNull(GraphQLString), description: "The duration of the video (in seconds)." }, released: { type: new GraphQLNonNull(GraphQLString), description: "Whether or not the video is released." } }, outputFields: { video: { type: videoType } }, mutateAndGetPayload: args => new Promise((resolve, reject) => { Promise.resolve(createVideo(args)) .then(video => resolve({ video })) .catch(reject); })});Let's stop here and have a look above...
- The
inputFieldsobject is basicaly the same as thevideoInputtype defined before - The
outputFieldswill correspond to what we can actually query on after mutation. In this case we will just write out thevideofield which will have avideoType. - The last part is the method
mutateAndGetPayload. Here the arguments are all theinputFieldsspecified above. The value that we end up returning or resolving from this method is what we're going to be able to pick out information from for these outputFields.
const mutationType = new GraphQLObjectType({ name: 'Mutation'. description: 'The root Mutation type', fields: { createVideo: videoMutation, },});Our last step here is to update our mutationType. Instead of having this config object for the createVideo field now we'll just have videoMutation.