DEV Community

Sean Walsh
Sean Walsh

Posted on • Edited on

How to write GraphQL middleware (Node, Apollo Server, Express)

In this article, we will use Node.js apollo-server-express with the graphql-middleware package.

I will assume that you are familiar with Node.js, Apollo server, Express, and ES6+ syntax.

I will skip most of the setup and assume you already have a GraphQL API set up with Apollo server. So let's install graphql-middleware and graphql-tools.

yarn add graphql-middleware graphql-tools
// or 
npm install graphql-middleware graphql-tools
Enter fullscreen mode Exit fullscreen mode

Then, create a middleware folder with index file. You can, of course, structure this however you like.

mkdir src/middleware && touch src/middleware/index.js
Enter fullscreen mode Exit fullscreen mode

Now, we have to add the middleware to the Apollo server constructor. So, navigate to your server.js file (or wherever you create your instance of Apollo).

First, import these functions:

import { applyMiddleware } from 'graphql-middleware';
import { makeExecutableSchema } from 'graphql-tools';
Enter fullscreen mode Exit fullscreen mode

Then add it to your instance of Apollo server:

import resolvers from './resolvers' // returns array of resolvers
import middleware from './middleware' // returns array of middelware

// this combines all of the resolvers
const executableSchema = makeExecutableSchema({ typeDefs: schema, resolvers });
const schemaWithMiddleware = applyMiddleware(executableSchema, ...middleware);

const server = new ApolloServer({
    playground: true,
    typeDefs: schema,
    resolvers,
    context: async ({ req, res }) => ({ req, res }), // now we can access express objects from apollo context arg 
    schema: schemaWithMiddleware, // add this property
});
Enter fullscreen mode Exit fullscreen mode

Okay, the setup is complete, now we are ready to write some middleware. In this example, we will create some middleware which will check the incoming request to the server includes a valid session cookie for user authentication.

Let's create a file in the middleware folder:

touch src/middleware/getUserFromCookie.js

Now, before we forget, let's import this file to middleware/index.js file:

import getUserFromCookie from './getUserFromCookie';

export default [getUserFromCookie];
Enter fullscreen mode Exit fullscreen mode

Let's make a plan for this module. I often like to write a brief plan in comments:

// TODO
// 1. get session cookie from express request object
// 2. use session id to get user details
// 3. add user to Apollo args
// 4. specify which resolvers to add the middleware to
Enter fullscreen mode Exit fullscreen mode

Now we're ready. Let's start with the number 1:

async function getUserFromCookie(req) {
  try {
    const { clientSession } = req.cookies; // requires cookie-parser middleware

    if (!clientSession) {
      throw new Error('session cookie does not exist');
    }

    return await getUser(clientSession); // get user details from Database
  } catch (error) {
    throw new AuthenticationError(`Cannot get user from cookie: \n ${error}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

What's going on here? Where does the req param come from!? Bear with me. We will call this function later and pass this argument.

To easily get access to your cookies, like we get here in this function, you will need to install the cookie-parser middleware package. I will leave this out of this article.

If this middlware cannot find any middleware, then we should block the client from getting any access to the api. We can use Apollo servers very helpful collection of predefined errors.

We will skip the getUser function in this article, since this is specific to how get user data in your api.

So, that covers 1. and 2. from our TODOs, let's move on to 3. Add user details to Apollo args. This should allow us to access the user details in the specified resolvers.

async function addUserToArgs(resolve, parent, args, context, info) {
  const user = await getUserFromCookie(context.req);
  const argsWithUser = { user, ...args };

  return resolve(parent, argsWithUser, context, info);
}
Enter fullscreen mode Exit fullscreen mode

This is the middleware function. Some points to note:

  • The four arguments being passed in to this function will be passed to all middleware.
  • Any code that comes before resolve will run before the resolver is executed
  • Any code after the resolve function will run after the resolver is executed
  • You can choose what arguments to pass to your resolver. In this case, we have added the user object to args, so the resolver can access args.user

At this point, you're probably wondering how to choose which resolvers use this middleware. This brings us to point number 4 from our TODOs.

We have to export an object which includes the resolver names as keys, and the middleware function as values. The graphql-middleware package will then work some magic to ensure this function is run on the specified resolvers.

export default {
  Query: {
    getUserDetails: addUserToArgs,
  },
  Mutation: {
    updateUserDetails: addUserToArgs,
  },
};
Enter fullscreen mode Exit fullscreen mode

Okay, we're almost done! But, you may be wondering at this point, what if I want to add some middleware to all resolvers (or a lot of resolvers), then this will quickly become tedious and very difficult to maintain as the api grows.

For this reason, I wrote a helper function which accepts as arguments an array of resolvers, and the middleware function. This will use the array reduce method to return one object with the resolver as the key and the middleware as the value. Here's how to use the helper function:

// import array of objects with Query and Mutaion properties
import resolvers from '../../resolvers';
import addMiddlewareToResolvers from './addMiddlewareToResolvers';

// pass array of resolvers and middleware function
export default addMiddlewareToResolvers(resolvers, addUserToArgs);

/*
  return {
    Query: {
      getUserDetails: addUserToArgs
      // rest of the queries
    },
    Mutation: {
      updateUserDetails: addUserToArgs
      // rest of the mutations
    }
  }
*/
Enter fullscreen mode Exit fullscreen mode

And here's the function(s). It's a little complex, if anyone can simplify this and make it more readable I'd love to see it!

import { ApolloError } from 'apollo-server-express'

// returns object with resolver names as keys, and middleware function as value
export default function addMiddleware(
  resolvers,
  middlewareFunction,
) {
  try {
    return resolvers?.reduce(
      (a, c) => buildResolverObject(a, c, middlewareFunction),
      {},
    )
  } catch (error) {
    throw new ApolloError(`Error in addMiddlewareToResolvers - ${error}`)
  }
}

function buildResolverObject(
  accumulator: any,
  { Query, Mutation },
  middlewareFunction: any,
) {
  const queryProperties = getResolverProperties(Query, middlewareFunction)
  const mutationProperties = getResolverProperties(Mutation, middlewareFunction)

  return {
    Query: {
      ...accumulator.Query,
      ...queryProperties,
    },
    Mutation: {
      ...accumulator.Mutation,
      ...mutationProperties,
    },
  }
}

function getResolverProperties(resolverObject = {}, middlewareFunction) {
  const keys = Object.keys(resolverObject)
  const properties = keys.map((key) => ({ [key]: middlewareFunction }))

  return properties.reduce((a, c) => ({ ...a, ...c }), {})
}
Enter fullscreen mode Exit fullscreen mode

That's all 🎉

Now you are ready to write your own custom middleware. Have fun!

P.S. Interested how to write integration tests using Jest for this middleware? Coming soon 😎

Top comments (9)

Collapse
 
omorhefere profile image
Omorhefere

Is this code available on GitHub?

Collapse
 
pprathameshmore profile image
Prathamesh More

Any links?

Collapse
 
mucorolle profile image
Muco Rolle Tresor

@seancwalsh I really want to see the content of addMiddlewareToResolvers

Collapse
 
zakariachahboun profile image
zakaria chahboun

good thanks

Collapse
 
seancwalsh profile image
Sean Walsh

You're welcome 🙂

Collapse
 
nickytonline profile image
Nick Taylor

Congrats on your first post!

1st place in Mariokart

Collapse
 
seancwalsh profile image
Sean Walsh

Thank you! 😄

Collapse
 
alejandro2396 profile image
Alejandro Moreno

@seancwalsh I would like to see the content of addMiddlewareToResolvers

Collapse
 
seancwalsh profile image
Sean Walsh

Hey Alejandro. Sorry about this, I missed the comment! I've updated the article now. Hope this helps