How to use GraphQL with Express.js#

This article is for developers who are familar with Express, but has little to no experience with GraphQL.

I will give a high-level introduction to GraphQL and some of its concepts that are basic to enabling a GraphQL endpoint in an Express application. Then, we will go ahead and create a simple Express app exposing an interactive GraphQL API endpoint.

GraphQL is a very vast topic with many different aspects to it, the implementation presented in this article priotizes on simplicity and learning rather than optimization for production scenarios.

What is GraphQL?#

GraphQL is a language-independent data request-response format specification. It is not a framework or a library, it is merely a specification of how requests to a backend should be made for accessing data and how those requests should be handled by the backend.

Packages like express-graphql on npm, are implemenations of the GraphQL specification. They are not GraphQL. Even you can create your own implementation of GraphQL with a name like yo-graphql, etc.

The official tagline of GraphQL says, "A query language for your API". Here, "query language" refers to the request format specification, and "API" refers to the data access layer, which could be an elaborate REST API or something as simple as users[2], where users is a variable in the app.

GraphQL key concepts#

Here are some of the key things about GraphQL. These will especially help you if you are familiar with REST, but are new to GraphQL.

1. GraphQL is API-centric#

REST is data-centric. Meaning, the design and architecture of REST revolves around the data. The data dictates the interface of the REST API.

GraphQL is API-centric. The design and architecture of GraphQL is driven by the requirement to have the best possible interface and experience for making requests to a backend system. Data is secondary, and is not a requirement.

All other points are basically an elaboration of this fact.

"Data is secondary, and is not a requirement" explained: An functional GraphQL endpoint can exist without having to interface with a database. In case of REST, if there is no database, there is no endpoint.

2. GraphQL is a single-endpoint API specification#

Unlike REST, which exposes multiple endpoints, GraphQL exposes a single POST endpoint, through which all the requests are served.

Think of the data behind the GraphQL API as a single gigantic object with numerous properties, which are called fields. This object is so huge that the models you are familiar with in REST, form the values of these fields (all the fields need not be models, though). In GraphQL-speak, the properties of these fields are known as arguments.

Do you notice that the terms 'fields' and 'arguments' sound like HTTP requests parameters and not data-related properties? That is because, it is so. Remember, the design of GraphQL is driven by API and not data.

Unless you enabled GraphiQL, the GraphQL browser, the only HTTP method GraphQL supports is the POST method. PUT, DELETE etc., are not used and have no meaning in GraphQL.

Data querying is done by sending a request called query.

Data creation, modification, and deletion is made by a request called mutation. Both happen over HTTP POST requests.

3. GraphQL can return more than one top-level model in a response#

A request to a REST API endpoint returns a single top-level model. Whereas, a GraphQL request can return more than one top-level model (if you requested for multiple fields and these fields were mapped to models).

While it may seem like a strange thing for a response to return more than one top-level model, this ability is what makes GraphQL a better choice than REST in many cases.

Consider a scenario, where you want to show the details about an author, all the books by the author, and the top five reviews of each book.

This would require three separate HTTP requests to get all the required data in a REST API request to a well normalized database,

In case of GraphQL, all these information can be returned in a single request. Even if the backend application server has to make three queries to the database server, it will be a much faster operation than the client making three separate HTTP requests to initiate those three database queries in case of REST API.

4. GraphQL can return a limited set of the properties of a model#

Except for some special cases, the object returned by a REST API response contains all the properties of a model. In short, the response object is a true represenation of the model.

In case of GraphQL, the response object need not be tied to a model. GraphQL will return an object with the properties specified in the arguments to a field.

5. GraphQL query and response structures must be specified beforehand#

Points number 2 and 3, while they make GraphQL sound pretty flexible, also seem utterly chaotic. How do you manage the chaos?

Enter GraphQL schema.

GraphQL schema is a implementation and structural definition for the request and response objects of a GraphQL system. This schema not only defines the structure (properties) of the query and response objects, but also the data type of the values of the properties. Meaning, GraphQL enforces strict typing.

This predefined structural restriction creates a predetermined system, which enables GraphQL implementations to enforce validation of requests and type checking of values.

6. GraphQL can act as a proxy to an existing API#

If you have a pre-existing REST API, you don't have to throw it away and start re-implenting it in GraphQL from scratch. You can simply put GraphQL infront of REST, making GraphQL a proxy to the REST API. That way you have a GraphQL API for the users, while GraphQL interacts with the database using the REST API.

7. GraphQL is not dependent on REST API#

The tagline, "A query language for your API", can give the wrong impression that, GraphQL can be useful to you only if you have a pre-existing REST API. "API" in this case be anything - a REST API, a database query function, the file system API, a key-value object, a static method, etc.

8. GraphQL is not a replacement of REST per se#

GraphQL is not meant to replace REST, just as mobile phones are not meant to replace desktop computers. Just as mobile phones can replace desktops in some cases, GraphQL can replace REST under certain scenarios.

You can use:

  • GraphQL with REST - GraphQL acts as a proxy to a REST API.
  • GraphQL along with REST - there is both a GraphQL endpoint (new clients) as well as a REST endpoint (older / legacy clients).
  • GraphQL instead of REST - a fresh new project that decided to go with GraphQL only right from the start.

Implementation in Express#

Two npm packages, graphql and express-graphql, are required to implement GraphQL API in Express.

  1. graphql provides the methods for enforcing types and compling GraphQL schema.
  2. express-graphql is a GraphQL implementation for Express.

We will be working with the following constants in the following sections.

const express = require('express');
const app = express();
const expressGraphQL = require('express-graphql');
const graphql = require('graphql');

const database = {
  users: ['Bill', 'Larry', 'Steve']
}

GraphQL uses a schema named query to implement the querying of the data it exposes, and a schema named mutation for making any changes (create, update, delete) to the data. You must specify atleast one of these schemas to get GraphQL working with Express.

This is how you load the express-graphql middleware and the GraphQL schema.

const schema = new graphql.GraphQLSchema({
  query: querySchema,
  mutation: mutationSchema
});

app.use('/graphql', expressGraphQL({
  schema: schema,
  graphiql: true
}));

We simply mount expressGraphQL as a middleware on the app at /graphql.

There are two undefined objects, querySchema and mutationSchema. What are those?

Query schema#

GraphQL schema and typed objects can be created using the GraphQL schema language too, which look something like this:

type User {
  name: String!
  id: Int!
}

However, I chose to use the dynamic way of creating schema and objects since it is the familiar "JavaScript way" compared to having to wrap your head around a new language.

The query schema is the definition of object types and implementation of resolvers for query (read) requests to fields.

Reminder: GraphQL API sits infront of a single giant object with many properties, which are conventionally called fields. A field may or may not correspond to a model.

Let's define a query schema named querySchema:

const querySchema = new graphql.GraphQLObjectType({
  name: 'Query',
  fields: {
    getUsers: {
      type: graphql.GraphQLList(graphql.GraphQLString),
      resolve: async function () {
        return database.users;
      }
    },
    getUser: {
      type: graphql.GraphQLString,
      args: {
        id: { type: graphql.GraphQLNonNull(graphql.GraphQLInt) }
      },
      resolve: async function (_, args) {
        if (args.id > database.users.length - 1) {
          throw new Error('Invalid id');
        }
        const id = args.id;
        return database.users[id];
      }
    },
    pi: {
      type: graphql.GraphQLFloat,
      resolve: async function () {
        return 22/7;
      }
    }
  }
});

A GraphQL schema is a GraphQL object, so we construct one using the graphql.GraphQLObjectType class.

I have named it "Query", but you can name it anything you like. Then we have defined the fields in the schema.

There are three fields:

  1. getUsers - returns the list (GraphQLList) of users in the "database", where each user stored as a string (GraphQLString)
  2. getUser - returns the user corresponding to the user id
  3. pi - returns the value of Pi as a floating point number (GraphQLFloat)

Query fields are analogous to the REST API endpoints or routes in Express; except, they are resolved via the request body, not HTTP method and URL.

To the create the "endpoints" of your GraphQL API, create the corresponding fields in the schema object.

A field must define its type (the data type of the object this field returns) and the resolve function, which is responsible for resolving the request; this function must return a value of the type specified in the type property.

The resolving functions can be defined on a property named rootValue too. However, I decided to use the resolve way because it is less distracting and self-contained.

Additionally, if a field can be queried using some parameters, they must be defined in the args property. The args property is a key-value pair object, where the parameter name forms the key, and the data type of the parameter forms the value.

If an argument in mandatory for a field, you can set as mandatory using the GraphQLNonNull method.

Mutation schema#

The mutation schema is the definition of object types and implementation of resolvers for mutation (create, update, delete) requests to fields. It is created in the same manner as the query schema, the only difference is in what the resolvers do.

Here is the mutation schema named mutationSchema:

const mutationSchema = new graphql.GraphQLObjectType({
  name: 'Mutation',
  fields: {
    addUser: {
      type: graphql.GraphQLString,
      args: {
        name: { type: graphql.GraphQLString }
      },
      resolve: async function (_, args) {
        const user = args.name;
        database.users.push(user);
        return user;
      }
    }
  }
});

Basic app#

This is the consolidation of all the code we have written so far.

app.js
const express = require('express');
const app = express();
const expressGraphQL = require('express-graphql');
const graphql = require('graphql');

const database = {
  users: ['Bill', 'Larry', 'Steve']
}

const querySchema = new graphql.GraphQLObjectType({
  name: 'Query',
  fields: {
    getUsers: {
      type: graphql.GraphQLList(graphql.GraphQLString),
      resolve: async function () {
        return database.users;
      }
    },
    getUser: {
      type: graphql.GraphQLString,
      args: {
        id: { type: graphql.GraphQLNonNull(graphql.GraphQLInt) }
      },
      resolve: async function (_, args) {
        if (args.id > database.users.length - 1) {
          throw new Error('Invalid id');
        }
        const id = args.id;
        return database.users[id];
      }
    },
    pi: {
      type: graphql.GraphQLFloat,
      resolve: async function () {
        return 22/7;
      }
    }
  }
});

const mutationSchema = new graphql.GraphQLObjectType({
  name: 'Mutation',
  fields: {
    addUser: {
      type: graphql.GraphQLString,
      args: {
        name: { type: graphql.GraphQLString }
      },
      resolve: async function (_, args) {
        const user = args.name;
        database.users.push(user);
        return user;
      }
    }
  }
});

const schema = new graphql.GraphQLSchema({
  query: querySchema,
  mutation: mutationSchema
});

app.use('/graphql', expressGraphQL({
  schema: schema,
  graphiql: true
}));

app.listen(3000, function(err) {
  if (err) console.log(err);
  else console.log('GraphQL API available at: http://localhost:3000/graphql');
});

Install the dependencies and start the app. You can access GraphiQL at http://localhost:3000/graphql.

Try making these requests:

{
  getUsers
}
mutation {
  addUser(name: "Jim")
}
{
  pi
}

There we have it, a very basic GraphQL endpoint in Express.

Example Express app with a GraphQL endpoint#

Real apps won't be as simple as the previous example. So here is a slightly more advanced app, which is closer to actual apps that you would be building.

app.js
const express = require('express');
const app = express();
const expressGraphQL = require('express-graphql');
const graphql = require('graphql');

const database = {
  users: {
    0: { id: 0, name: 'Georges', rank: 1 },
    1: { id: 1, name: 'Anderson', rank: 2 },
    2: { id: 2, name: 'Khabib', rank: 3 },
    3: { id: 3, name: 'Conor', rank: 4 }
  },
  quotes: [
    { message: 'I am not impressed by your performance.', uid: 0, featured: true },
    { message: 'I back!', uid: 1 },
    { message: 'Is normal.', uid: 1 },
    { message: 'I predict dese tings.', uid: 3 },
    { message: '60 Gs, baby!', uid: 3 },
    { message: 'Just send location.', uid: 2 },
    { message: 'I will smesh him.', uid: 2 },
    { message: 'I care about my legacy.', uid: 2 },
  ]
};

const UserType = new graphql.GraphQLObjectType({
  name: 'User',
  fields: {
    name: { type: graphql.GraphQLString },
    rank: { type: graphql.GraphQLInt }
  }
});

const QuoteType = new graphql.GraphQLObjectType({
  name: 'Quote',
  fields: {
    message: { type: graphql.GraphQLString },
    uid: { type: graphql.GraphQLInt },
    published: { type: graphql.GraphQLBoolean }
  }
});

const querySchema = new graphql.GraphQLObjectType({
  name: 'Query',
  fields: {
    getUsers: {
      type: graphql.GraphQLList(UserType),
      resolve: async function () {
        return Object.values(database.users);
      }
    },
    getUser: {
      type: UserType,
      args: {
        id: { type: graphql.GraphQLInt }
      },
      resolve: function (_, args) {
        return database.users[args.id];
      }
    },
    getQuote: {
      type: QuoteType,
      args: {
        id: { type: graphql.GraphQLInt }
      },
      resolve: function (_, args) {
        return database.quotes[args.id];
      }
    },
    getQuotes: {
      type: graphql.GraphQLList(QuoteType),
      args: {
        uid: { type: graphql.GraphQLInt },
        limit: { type: graphql.GraphQLInt }
      },
      resolve: function (_, args) {
        const result = [];
        if (typeof args.uid === 'undefined') {
          database.quotes.forEach(quote => {
            if (quote.published) result.push(quote);
          });
          return result;
        }

        const limit = args.limit || database.quotes.length;
        database.quotes.forEach(quote => {
          if (quote.uid === args.uid && result.length < limit) {
            result.push(quote);
          }
        });
        return result;
      }
    },
    getFeatured: {
      type: QuoteType,
      resolve: function (_, args) {
        let result;
        for (let i = 0; i < database.quotes.length; i++) {
          const quote = database.quotes[i];
          if (quote.featured) {
            result = quote;
            break;
          }
        }
        return result;
      }
    }
  }
});

const QuoteInput = new graphql.GraphQLObjectType({
  name: 'QuoteInput',
  fields: {
    message: { type: graphql.GraphQLString },
    uid: { type: graphql.GraphQLInt }
  }
});

const mutationSchema = new graphql.GraphQLObjectType({
  name: 'Mutation',
  fields: {
    addQuote: {
      type: QuoteInput,
      args: {
        message: { type: graphql.GraphQLString },
        uid: { type: graphql.GraphQLInt }
      },
      resolve: async function (_, args) {
        const newQuote = {
          message: args.message,
          uid: args.uid
        };
        database.quotes.push(newQuote)
        return newQuote;
      }
    }
  }
});

const schema = new graphql.GraphQLSchema({
  query: querySchema,
  mutation: mutationSchema
});

app.use('/graphql', expressGraphQL({
  schema: schema,
  graphiql: true
}));

app.listen(3000, function(err) {
  if (err) console.log(err);
  else console.log('GraphQL API available at: http://localhost:3000/graphql')
});

Some requests to try in the GraphiQL explorer:

{
  getQuotes (uid: 2, limit: 2) {
    message
  },
  getFeatured {
    message
  }
}
{
  getFeatured {
    message,
    uid
  }
}
{
  getUsers {
    name,
    rank
  }
}
mutation {
  addQuote (message: "I walk the talk!", uid: 3) {
    message
  }
}
{
  getQuotes (uid: 3) {
    message
  }
}

Summary#

GraphQL is an excellent API paradigm. Compared to REST, it is a very different approach and much more flexible and efficient way of implementing APIs.

Using the graphql package we can create GraphQL objects and GraphQL schema. The express-graphql package is a GraphQL implementation for Express, loading it as a middleware enables a GraphQL endpoint on the app.

References#

  1. https://graphql.org/
  2. https://graphql.org/learn/schema/
  3. https://graphql.org/graphql-js/
  4. https://graphql.org/graphql-js/graphql-clients/
  5. https://www.npmjs.com/package/graphql
  6. https://www.npmjs.com/package/express-graphql
Tweet this | Share on LinkedIn |