GraphQL-to-MongoDB, or how I learned to stop worrying and love generated query APIs

In this post we’ll have a look at leveraging GraphQL types to expose MongoDB capabilities in NodeJs. We will also examine graphql-to-mongodb, the unobtrusive solution we came up with for our service, and its rationales.

The advent of the GraphQL MongoDB stack

Relative newcomer GraphQL and established MongoDB are two technologies that appear well suited to one another. MongoDB is a document-oriented DB with a flexible query language. GraphQL is a service API and a query language at the same time. It works by defining a hierarchical, typed, and parameterized schema. Both technologies take and receive arguments in a hierarchical data structure. Taken together, the two compatible technologies can make for a streamlined interface between your client and data.

First off, if you feel that your knowledge of either technology could use a refresher, you’re welcome to read more about GraphQL or MongoDB before continuing.

Growing pains

Implementing a way to expose MongoDB querying on a GraphQL backend is not a trivial task; we learned that when we tried to join the two in our latest NodeJs service. It’s easy enough to start by adding a single query field, using a single comparison operator, one at a time. As the query complexity of your clients increases, however, you can easily find yourself maintaining a disorganized mess of filtering code.

A developer might be tempted to simply accept a generic JSON type as input, passing a client’s input directly to MongoDB, but we saw that kind of solution to be less than satisfactory. Not only does that approach miss the entire point of GraphQL, it also gives up control over how the client may communicate with the DB.

Our ideal API

Upon recognizing that the issue was less than simple, we set out on a search for a solution that suited our needs, which were:

  • An interface to MongoDB’s powerful querying capabilities
  • Simple implementation
  • Unobtrusive integration
  • Explicitness and consistency to the GraphQL type schema
  • No vulnerabilities to NoSQL injections

Unfortunately, our search yielded less than fruitful results.

If you want something done right…

As is often the case, we couldn’t find a mature and well-documented third-party solution that met our needs, prompting us to design one ourselves. Subsequently leading us to come up with an answer in the form of the package graphql-to-mongodb, publicly available on both GitHub and npmFundamentally, the package works by generating query arguments for your GraphQL schema at run-time, based on your existing GraphQL types. It parses the sent requests into MongoDB query params.

Let’s explore how it checks off the needs we identified earlier:

MongoDB for your client

The package boosts your GraphQL API with the bulk of MongoDB’s most commonly used query operators. With it, a client can comb through the underlying data in a multitude of ways without requiring additional changes to your API for every new query.

An example GraphQL query sent to a service using the package, showcasing filtering, sorting, and pagination:

{
    person (
        filter: {
            age: { GT: 18 },
            name: { 
                firstName: { EQ: "John" } 
            }
        },
        sort: { age: DESC },
        pagination: { limit: 50 }
    ) {
        name { 
            lastName
        }
        age
    }
}

Queries 50 people, oldest first, over the age of 18, and whose first name is John

All of that, and more, for a very small development overhead in your service.

Simplexity

As with many packages, it strives to give you the biggest bang for your buck, hiding the complexities of the solution behind a simple integration. The exposed GraphQL field will be based on your underlying GraphQL type describing the data structure schema.

new GraphQLObjectType({
    name: 'PersonType',
    fields: () => ({
        age: { type: GraphQLInt },
        name: {
            type: new GraphQLNonNull(new GraphQLObjectType({
                name: 'NameType',
                fields: () => ({
                    firstName: { type: GraphQLString },
                    lastName: { type: GraphQLString }
                })
            }))
        }
    })
});

Given a simple GraphQL type

When implementing the package, for the most common use case, all you need to do is to build a field in the GraphQL schema with a wrapped resolve function (getMongoDbQueryResolver) and generated arguments (getGraphQLQueryArgs).

person: {
    type: new GraphQLList(PersonType),
    args: getGraphQLQueryArgs(PersonType),
    resolve: getMongoDbQueryResolver(PersonType,
        async (filter, projection, options, source, args, context) =>
            await context.db.collection('person')
                .find(filter, projection, options).toArray()
    )
}

We’ll add the following field to our schema

That’s it!

For the price of two function calls, you’ve just added all of the functionality described in the previous section to your API.

The additional arguments supplied by the wrapper – filter, projection, and options – can be passed directly to MongoDB! To get an idea of what the package does, take a look at these arguments, produced from the previous section’s query:

// filter
{
  "age": {
    "$gt": 18
  },
  "name.firstName": {
    "$eq": "John"
  }
}
// projection
{
  "name.lastName": 1,
  "age": 1
}




// options
{
  "sort": {
    "age": -1
  },
  "limit": 50
}


It’s just middleware

It’s clearly visible that the package behaves like a middleware. This feature allows for the development of modules independent of MongoDB in the GraphQL service. 

Fields built using the package’s function can be easily extended. It’s simple enough to merge additional arguments into the generated args object, and to add handling in the resolver.

person: {
    type: new GraphQLList(PersonType),
    args: Object.assign({},
        { id: { type: GraphQLString } }, 
        getGraphQLQueryArgs(PersonType)
    ),
    resolve: getMongoDbQueryResolver(PersonType,
        async (filter, projection, options, obj, args, context) => {
            if (args.id) filter.id = id;
            return await context.db.collection('persons')
                .find(filter, projection, options).toArray();
        })
}

Resolving fields within your GraphQL type is also supported, though it requires a minor overhead in defining the field. One of the added benefits of the package is a minimization of throughput by projecting from MongoDB only the fields requested by the user. For resolved fields, what that means is that their dependencies might not always be queried from the DB. To resolve that issue, the package allows you to define a resolve field’s dependencies to ensure that when that field is queried, its dependencies will always be retrieved as well.

fullName: {
    type: GraphQLString,
    resolve: (obj, args, { db }) => 
        `${obj.name.firstName} ${obj.name.lastName}`,
    dependencies: ['name'] // or ['name.firstName', 'name.LastName']
}

Alternatively, if throughput is of no concern, the projection argument supplied by the resolve wrapper can simply be discarded and replaced by an empty object.

Well-defined…

Because the functionality of the package is based solely on the GraphQL types of your implementation, the exposed API of the service is both explicit and consistent.

A description of the types generated by graphql-to-mongodb from the examined code sample, as viewed in graphiQL

Because of course there’s mutation functionality…

Only the fields defined in the original GraphQL type (to the far left above) are exposed as arguments in the schema fields. Likewise, generated input and insert types provided as an additional functionality of the package are derived directly from your original type and grant mutation capabilities on its fields to your API.

…and safe

The explicit nature of the API provides it with a measure of security. GraphQL provides out-of-the-box input validation, ensuring that all arguments match the types defined by your schema. With every one of your fields being unambiguously defined and consistently processed, would-be attackers are left with no wiggle room to exploit, or human errors to target.

Give it a try

Next time you’re designing a new service, or just considering an overhaul of an existing one, reflect on the benefits and principles of the package. If you want to give your NodeJs GraphQL service a whole lot of the power of the MongoDb database you have standing behind it with very little hassle, mayhaps you’ll consider adding graphql-to-mongodb to your implementation.

Previous

Israeli high-tech through American eyes

Next

Applying microservices design patterns to scale react app development

11 Comments

  1. jon

    Really great stuff! I’m very interested in graphql-to-mongoose generators and vice versa (mongoose-to-graphql). bit of a noob question but… does graphql-to-mongodb generate a schema/model under the hood? or did you manage to bypass that step? or do I just not understand what graphql-to-mongodb does? thanks for your time!

    • Yoav Karako

      To answer your questions in order: No. Yes. I’m not sure.
      There is a sort of a schema generated under the hood, but it’s made up of purely GraphQL Types. That schema is expressed as the API arguments exposed by a service using the package. That schema is also used in the process of parsing said arguments into a MongoDB query. If you’re using mongoose, it’s schema/model features are being completely bypassed, the package creates query objects that should be passed directly to MongoDB.

      • Joshua Scott

        How would you get this to work with Apollo? I am in a need of a solution to build a advance search type feature. So using the filters, sort, limit, skip, etc.

        • Yoav Karako

          Hi Scott!
          There’s really nothing special about the GraphQL API you can create with the package, so there should be no problem to consume it with any Apollo client.
          For advanced filtering, beyond the capabilities described in this post, MongoDB OR/AND operators are supported as well.
          It’s best to look at the API with a tool that uses GraphQL introspection (like graphiql) to get a full understanding of what you can do with it.
          If you find anything missing from the API that you think is worth adding we’d love to hear about it.

  2. Claude

    Hey Yoav, great stuff man.
    Can we have an example of how to use the package for mutations?

    Chers!

    • Yoav Karako

      Hi Claude,
      Thanks a bunch.
      There are a few examples as to how you could implement mutations using the package in this github issue.

  3. Hey Yoav,
    thanks a lot to share it, I found it really interesting. I was wondering if it’s possible to use it with apollo-server

    • Yoav Karako

      Hi Daniele,
      Thanks for reading. There’s no reason for apollo-server to not work. I mostly use express-graphql, but a quick check showed apollo-server-express to work just fine.

  4. Matt

    Thank you so much for sharing this, Yoav!
    This is awesome!!

    I just wrote and queried my first query with this and i’m already loving it 🙂

  5. Benny

    Hi 🙂 I just discovered this today thanks for this. I am currently a similar project for practice and I will be implementing similar functionality for mine. I like your abstractions, I am building mine in Apollo with React Apollo frontend in view of adding Subscriptions later

  6. Martin

    Hey , I’m a newbie to node ( ex Microsoft dev ) and I’m a bit lost with this. Do you have a step -by-step example on how to set up a Graphql server that links to Mongodb. It looks really good , but I am not advanced anouth to figure out how to put it all together.

Leave a Reply

Your email address will not be published. Required fields are marked *

Powered by WordPress & Theme by Anders Norén