Using GraphQL with MongoDB


This post originally appeared on the RisingStack blog.

With the Mongoose adapter for Graffiti, you can use your existing Mongoose schema for developing a GraphQL application.

We are going to cover the following topics:

  • Introduction to Graffiti
  • The Mongoose adapter
  • Relay & GraphQL
  • Getting started with Graffiti
  • Graffiti TodoMVC - a Relay example

Introduction to Graffiti

Developers generally tend to be lazy. We usually don’t write boilerplate code. GraphQL is great, but manually specifying the schema can be painful. That’s the reason we created Graffiti.

using_graphql_with_mongodb_graffiti_mongoose

Graffiti consists of two main components. You can use graffiti to add a GraphQL endpoint to your web server. Either you can use a schema generated by an adapter, or you can pass in your own. An adapter like graffiti-mongoose can generate the GraphQL schema from your database specific schema description.

The Mongoose adapter for Graffiti

Graffiti currently has an adapter for the Mongoose ORM - with more adapters to come later.

Graffiti Mongoose can help you to use your existing Mongoose schema to generate a Relay compatible GraphQL schema.

We will use the following schema throughout this blog post:

import mongoose from 'mongoose';

const UserSchema = new mongoose.Schema({
  name: {
    type: String
  },
  age: {
    type: Number,
    index: true
  },
  createdAt: {
    type: Date,
    default: Date.now()
  },
  friends: [
    {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'User'
    }
  ]
});

const User = mongoose.model('User', UserSchema);

export default User;

The generated GraphQL schema looks the following:

input addUserInput {
  name: String
  age: Float
  createdAt: Date
  friends: [ID]
  clientMutationId: String!
}

type addUserPayload {
  viewer: Viewer
  changedUserEdge: changedUserEdge
  clientMutationId: String!
}

type changedUser {
  name: String
  age: Float
  createdAt: Date
  friends(after: String, first: Int, before: String, last: Int, name: String, age: Float, createdAt: Date, _id: ID): friendsConnection
  _id: ID
  id: ID!
}

type changedUserEdge {
  node: changedUserNode
  cursor: String!
}

type changedUserNode {
  name: String
  age: Float
  createdAt: Date
  friends(after: String, first: Int, before: String, last: Int, name: String, age: Float, createdAt: Date, _id: ID): friendsConnection
  _id: ID
  id: ID!
}

scalar Date

input deleteUserInput {
  id: ID!
  clientMutationId: String!
}

type deleteUserPayload {
  viewer: Viewer
  ok: Boolean
  id: ID!
  clientMutationId: String!
}

type friendsConnection {
  pageInfo: PageInfo!
  edges: [friendsEdge]
  count: Float
}

type friendsEdge {
  node: User
  cursor: String!
}

interface Node {
  id: ID!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

type RootMutation {
  addUser(input: addUserInput!): addUserPayload
  updateUser(input: updateUserInput!): updateUserPayload
  deleteUser(input: deleteUserInput!): deleteUserPayload
}

type RootQuery {
  viewer: Viewer
  node(id: ID!): Node
  user(id: ID!): User
  users(id: [ID], ids: [ID], name: String, age: Float, createdAt: Date, _id: ID): [User]
}

input updateUserInput {
  name: String
  age: Float
  createdAt: Date
  friends: [ID]
  id: ID!
  clientMutationId: String!
}

type updateUserPayload {
  changedUser: changedUser
  clientMutationId: String!
}

type User implements Node {
  name: String
  age: Float
  createdAt: Date
  friends(after: String, first: Int, before: String, last: Int, name: String, age: Float, createdAt: Date, _id: ID): friendsConnection
  _id: ID
  id: ID!
}

type UserConnection {
  pageInfo: PageInfo!
  edges: [UserEdge]
  count: Float
}

type UserEdge {
  node: User
  cursor: String!
}

type Viewer implements Node {
  id: ID!
  users(after: String, first: Int, before: String, last: Int, name: String, age: Float, createdAt: Date, _id: ID): UserConnection
  user(id: ID!): User
}

The road towards Relay compatibility

Relay is a framework for building data-driven React applications. You can declare your data requirements using GraphQL for each component and Relay handles the requests efficiently. Relay makes a few assumptions about the GraphQL schema that is provided by the GraphQL server.

The Node interface

Every type must implement the Node interface, which contains a single id field. This is a globally unique identifier encoding the type and type-specific ID. This makes it possible to re-fetch objects using only the id.

query ReFetch {
  node(id: "VXNlcjo1NjQwOThlNTI3ZjUyYTg0NTBiNWE5NDQ=") {
    __typename
    ... on User {
      name
      age
    }
  }
}
{
  "data": {
    "node": {
      "__typename": "User",
      "name": "User0",
      "age": 0
    }
  }
}

Pagination and the Connection type

The pagination and slicing rely on the standardized Connection type. We can use an introspection query to see what it looks like.

query ConnectionTypeIntrospection {
  __type(name: "UserConnection") {
    name
    fields {
      name
      type {
        kind
        ofType {
          name
          kind
          fields {
            name
          }
        }
      }
    }
  }
}
{
  "data": {
    "__type": {
      "name": "UserConnection",
      "fields": [
        {
          "name": "pageInfo",
          "type": {
            "kind": "NON_NULL",
            "ofType": {
              "name": "PageInfo",
              "kind": "OBJECT",
              "fields": [
                {
                  "name": "hasNextPage"
                },
                {
                  "name": "hasPreviousPage"
                },
                {
                  "name": "startCursor"
                },
                {
                  "name": "endCursor"
                }
              ]
            }
          }
        },
        {
          "name": "edges",
          "type": {
            "kind": "LIST",
            "ofType": {
              "name": "UserEdge",
              "kind": "OBJECT",
              "fields": [
                {
                  "name": "node"
                },
                {
                  "name": "cursor"
                }
              ]
            }
          }
        },
        {
          "name": "count",
          "type": {
            "kind": "SCALAR",
            "ofType": null
          }
        }
      ]
    }
  }
}

The edge type describes the collection, and the pageInfo contains metadata about the current page. We have also added a count field, which can be very handy in certain situations. Slicing is done via the passed in arguments: first, after, last and before. For example, we could ask for the first two users after a specified cursor on the viewer root field.

query UsersPagination {
  viewer {
    users(first: 2, after: "Y29ubmVjdGlvbi41NjQwOWMwZjU1MzBmOGFhNTBlM2NjZWE=") {
      count
      edges {
        cursor
        node {
          name
        }
      }
    }
  }
}
{
  "data": {
    "viewer": {
      "users": {
        "count": 98,
        "edges": [
          {
            "cursor": "Y29ubmVjdGlvbi41NjQwOWMwZjU1MzBmOGFhNTBlM2NjZWI=",
            "node": {
              "name": "User2"
            }
          },
          {
            "cursor": "Y29ubmVjdGlvbi41NjQwOWMwZjU1MzBmOGFhNTBlM2NjZWM=",
            "node": {
              "name": "User3"
            }
          }
        ]
      }
    }
  }
}

Mutations

Mutations like add, update and delete are also supported. Let’s try to add a new user!

mutation AddUser {
  addUser(input: {clientMutationId: "1", name: "New Usr"}) {
    changedUserEdge {
      node {
        id
        name
        createdAt
      }
    }
  }
}
{
  "data": {
    "addUser": {
      "changedUserEdge": {
        "node": {
          "id": "VXNlcjo1NjQwYTJiNTU1MzBmOGFhNTBlM2NkNGQ=",
          "name": "New Usr",
          "createdAt": "2015-11-09T13:13:51.523Z"
        }
      }
    }
  }
}

As you can see, we just made a typo. We can fix the user’s name by using an update mutation.

mutation FixName {
  updateUser(input: {clientMutationId: "2", id: "VXNlcjo1NjQwYTJiNTU1MzBmOGFhNTBlM2NkNGQ=", name: "New User"}) {
    changedUser {
      name
    }
  }
}
{
  "data": {
    "updateUser": {
      "changedUser": {
        "name": "New User"
      }
    }
  }
}

Nice, isn’t it?

Resolve hooks

Most likely you’ll need some custom logic in your application. For example, to authorize a request or to filter certain fields before returning it to the client. You can specify pre- and post-resolve hooks to extend the functionality of Graffiti Mongoose. You can add hooks to type fields and query fields (singular & plural queries, mutations) too. By passing arguments to the next function, you can modify the parameters of the next hook or the return value of the resolve function.

For example, this pre-mutation hook filters bad words.

const filter = new BadWordsFilter();
const hooks = {
  mutation: {
    pre: (next, todo, ...rest) => {
      if (todo.text) {
        todo.text = filter.clean(todo.text);
      }

      next(todo, ...rest);
    }
  }
};
const schema = getSchema(mongooseSchema, {hooks});

Creating a GraphQL server

First, we need to define our Mongoose models.

We all like pets, right? For our application, we’ll keep track of users and pets. Let’s define the User and Pet models!

import mongoose from 'mongoose';

const PetSchema = new mongoose.Schema({
  name: {
    type: String
  },
  type: {
    type: String
  },
  age: {
    type: Number
  }
});

const Pet = mongoose.model('Pet', PetSchema);

export default Pet;
import mongoose from 'mongoose';

const UserSchema = new mongoose.Schema({
  name: {
    type: String
  },
  age: {
    type: Number,
    index: true
  },
  friends: [{
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  }],
  pets: [{
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Pet'
  }],
  createdAt: {
    type: Date,
    default: Date.now
  }
});

const User = mongoose.model('User', UserSchema);

export default User;

We can generate the GraphQL schema from the Mongoose models using graffiti-mongoose.

import mongoose from 'mongoose';
import User from './user';
import Pet from './pet';
import {getSchema} from [email protected]/graffiti-mongoose';

mongoose.connect(process.env.MONGO_URI || 'mongodb://localhost/graphql');

export default getSchema([Pet, User]);

Now, we can add graffiti to the project.

import express from 'express';
import graffiti from '../';
import schema from './schema';

const app = express();
app.use(graffiti.express({
  schema
}));

app.listen(3001, (err) => {
  if (err) {
    throw err;
  }

  console.log('Express server is listening on port 3001');
});

Our server is ready to use. You can use GraphiQL, an in-browser GraphQL IDE, to explore our GraphQL API by navigating to localhost:3001/graphql.

You can find examples for koa and hapi alongside express in the main repository.

TodoMVC

To demonstrate the Relay compatibility, we also created a Relay application based on the well known TodoMVC. The source code can be found here.

You can take a look here if you want to try it out: graffiti-todo.herokuapp.com.