Sign in
Log inSign up

Write your own GraphQL directive in Apollo Server

Wan's photo
Wan
·Jun 26, 2019

Have you seen a schema like this before:

type User {
  id: ID! @unique
  stories: [Story!]!
}

type Story {
  id: ID! @unique
  text: String!
}

In GraphQL, @ is used to denote a directive. Here's a simple description given by the specification:

Directives provide a way to describe alternate runtime execution and type validation behavior in a GraphQL document.

Similar to resolvers, the GraphQL specification doesn't specify the directives implementation and it's up to the developers to handle that. In the above example, @unique is used to tell the server to generate a unique ID; the implementation of that ID generation is done by the server itself.

If we put @unique to the text field, we will expect that each story will have a unique text:

type User {
  id: ID! @unique
  stories: [Story!]!
}

type Story {
  id: ID! @unique
  text: String! @unique
}

There is a lot more details related to directives and if you're interested to learn them, you can start by reading the specification in the following sections: 2.12, 3.13, and 5.7.

Instead of going into a rabbit hole, in this post, I'll show you how to implement a simple directive in Apollo Server that will take the result of our GraphQL query and encrypt it.

Obligatory Boring Setup Section

Typical to most tutorials, make a directory for this project. After changing to that directory, run npm with the following commands:

  1. Initialise the project with npm init -y
  2. Install graphql-yoga and bcryptjs with npm install graphql-yoga bcryptjs. graphql-yoga is based on apollo-server and you don't need to complicated setup to get it working

Next, create an app.js file in the root of our project directory. Then, in our package.json, add "start": "node app.js" under the "scripts" section.

In app.js, add the following boilerplate code for the creation of our server:

const { GraphQLServer } = require("graphql-yoga")

const typeDefs = `
  type Query {
    receiveMessage(message: String!): String!
  }
`

const resolvers = {
  Query: {
    receiveMessage: (_, { message }) => message,
  },
}

const server = new GraphQLServer({ typeDefs, resolvers })
server.start(() => console.log("Server is running on localhost:4000"))

Run npm start and the GraphQL Playground will be available in localhost:4000.

Try the receiveMessage query in the playground to make sure that everything works:

query {
    receiveMessage(message: "A cryptic message")
}

If you're not sure you're doing it right, check this repo for the completed project.

Now that the boring stuff is over, let's write the directive.

Defining the directive in the schema

Any directive should be included in our schema. In apollo-server, which is what graphql-yoga is based on, we can pass your schema definition ito typeDefs.

Here's our current typeDefs:

// app.js

const typeDefs = `
  type Query {
    receiveMessage(message: String!): String!
  }
`

To add a new directive, we can add directive @name-of-directive on FIELD_DEFINITION. Your typeDefs will now look something like this:

directive @encrypt on FIELD_DEFINITION

type Query {
    receiveMessage(message: String!): String!
 }

Similar to how we put type to denote the type of queries,, we use directive to denote the available directives.

Directives cannot be applied to anything within the schema unless we specify its target.

A GraphQL schema is not made up of fields only, it can also contain enum, interface, union, and more. Apollo exposes those parts of the schema (Type System Definitions or locations) via the subclasses of SchemaDirectiveVisitor (read about the available subclasses here).

In our case, we are indicating that @encrypt will apply to the schema fields by writing on FIELD_DEFINITION. Later, if we want the directive to apply to a different location, for example to arguments, we can change it to on ARGUMENT_DEFINITION.

Writing the directive implementation

We will need to use SchemaDirectiveVisitor from graphql-tools (included with graphql-yoga) in order to complete this section.

Create a file in the root of your project named directives.js and import the following:

const { SchemaDirectiveVisitor } = require("graphql-tools")
const { defaultFieldResolver } = require("graphql")
const bcrypt = require("bcryptjs")

We need SchemaDirectiveVisitor because any directive we make should be a subclass of SchemaDirectiveVisitor.

defaultFieldResolver is needed in case our field doesn't have its own resolver.

bcrypt will be used to encrypt the result we receive from the query's resolver.

Once you're done, be ready because I'm going to spit out the block of code representing our directive implementation:

class EncryptDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    const { resolve = defaultFieldResolver } = field
    field.resolve = async function(...args) {
      const result = await resolve.apply(this, args)
      if (typeof result === "string") {
        return bcrypt.hashSync(result, 8)
      }
      return result
    }
  }
}

What is going on here?

First, we create a directive which is a subclass of SchemaDirectiveVisitor. We can change the subclass name to something else if we want to.

Second, remember that our directive can only apply to the locations that we've indicated in our schema. Once we've done that, we'll have to write what the directive will do when it is applied to a specified location.

If we use on FIELD_DEFINITION in the schema, then our directive implementation should be within visitFieldDefinition. The method will take in the field object and you can do many things with that. In our case, we want to override the field's resolver.

Third, we will use the destructuring assignment to extract the resolve function from field. If there's no resolve function, it will default to defaultFieldResolver.

Fourth, we will override the field's resolve function. The first thing we do is we run the field's own resolver so that we can store the initial result. If you're not sure what .apply means, MDN has a wonderful explanation here.

Fifth, we'll do a light typechecking for string before we encrypt the result and return it. If it is not a string, we will return the normal message.

After everything's finished, be sure to export the class:

module.exports = {
  EncryptDirective,
}

Adding the directive definition to our server

Before we can use the directive, let’s import it first:

const { EncryptDirective } = require("./directives");

Next, look at your server configuration in app.js:

const server = new GraphQLServer({
  typeDefs,
  resolvers,
})

Aside from the type definition and resolver, our server can also be configured with a directive via the schemaDirectives option. Setting it up is easy:

const server = new GraphQLServer({
  typeDefs,
  resolvers,
  schemaDirectives: {
    encrypt: EncryptDirective,
  },
})

You can link the EncryptDirective to any name you want. If you use makeItGibberish: EncryptDirective, then you can invoke the directive via @makeItGibberish. Just make sure you change the directive definition in the schema as well .

Alright, before we apply the directive to our schema, reset the server, open the GraphQL playground, and test the receiveMessage query to make sure it still works.

If everything’s good, change your typeDefs to the following:

directive @encrypt on FIELD_DEFINITION

type Query {
    receiveMessage(message: String!): String! @encrypt
 }

Reset the server and test the query again. If you receive encrypted (aka seemingly nonsensical) string, congratulations - you have built your own custom GraphQL directive.

And do check this repo for the completed project if you're stuck at any point.

If you’re interested to build more of your own directives in Apollo Server, be sure to check these examples from Apollo’s doc. You can also find open-sourced community-built directives in the graphql-community repo.


This post was originally published on my website.

Sign up for my newsletter if you want more articles on React + GraphQL.


Thank you for reading. I really appreciate it.

Feel free to contact me or leave a comment if you need any help; leave a criticism too if I'm wrong in anything (this is highly likely).