Middleware in GraphQL Apollo Server

A Series Blog for Deep Dive into Apollo GraphQL from Backend to Frontend

E.Y.
5 min readApr 29, 2021
Photo by Mink Mingle on Unsplash

This is a 5th of the series blogs on deep dive into Apollo GraphQL from backend to frontend. A lot of information is Apollo GraphQL Doc or GraphQL Doc as well as their source code on Github — all tributes go to them. For my part, I would like to give you my “destructuring” of the original knowledge and my reflection on it, analysis on the code examples/source code as well as some extra examples.

Apollo Server is an opinionated GraphQL server implementation. According to the doc, you can use Apollo Server as:

  • A stand-alone GraphQL server, including in a serverless environment
  • An add-on to your application’s existing Node.js middleware (such as Express or Fastify)
  • A gateway for a federated data graph

In the 2nd option, Apollo Server comes with a few NodeJS middleware options, one of them being the popular apollo-server-express . Note that since apollo-server v2, it already handles setting up an HTTP server for you. But you can still hook it up with a NodeJS http framework with any of this list of middleware options.

This brings a some additional benefits that apollo-server alone cannot provide. Such as the rich middleware ecosystem. Speaking of which, the Apollo Server itself is also built with Express.

A quick reminder of what middleware is:

class Middleware {
constructor() {
this.middlewares = [];
}
use(fn) {
this.middlewares.push(fn);
}
executeMiddleware(data, done) {
this.middlewares.reduceRight((done, next) => () => next(data, done), done)
(data);
}
run(data) {
this.executeMiddleware(data, done => console.log(data));
}
}

The biggest downside is that apollo-server is faster than apollo-server-express as there are more code to compile and execute in the apollo-server-express integration.

Let’s say an example of using middlewares usingapollo-server-express :

const express = require('express');
const { ApolloServer, gql } = require('apollo-server-express');
const PORT = 4000;const app = express();const typeDefs = gql`
type Query {
hello: String
}
`;
const resolvers = {
Query: {
hello: () => 'Hello world!'
},
};
app.use(“/”, express.static(path.join(__dirname, “public”)));const server = new ApolloServer({ typeDefs, resolvers });// or app.use(server.getMiddleware({}))
server.applyMiddleware({ app });
app.listen({ port: PORT }, () =>
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`)
)

What applyMiddleware actually do is only add middleware to the path (default /graphql router), so it’s not applied to the whole app. But app.use(“/”, middlewareFunc) will apply to the whole app as it is applied to the Express app directly. Essentially, apollo-server is still a middleware that bridges the HTTP layer with the GraphQL engine provided by GraphQL.js.

Looking deeper at the source code of the apollo-server-express applyMiddleware function, you can find that there’s only a set of middleware you can apply (in the GetMiddlewareOptions )

import express from 'express';
import corsMiddleware from 'cors';
import { json, OptionsJson } from 'body-parser';
import {
renderPlaygroundPage,
RenderPageOptions as PlaygroundRenderPageOptions,
} from '@apollographql/graphql-playground-html';
import {
GraphQLOptions,
FileUploadOptions,
ApolloServerBase,
formatApolloErrors,
processFileUploads,
ContextFunction,
Context,
Config,
} from 'apollo-server-core';
import { ExecutionParams } from 'subscriptions-transport-ws';
import accepts from 'accepts';
import typeis from 'type-is';
import { graphqlExpress } from './expressApollo';
export { GraphQLOptions, GraphQLExtension } from 'apollo-server-core';export interface GetMiddlewareOptions {
path?: string;
cors?: corsMiddleware.CorsOptions | corsMiddleware.CorsOptionsDelegate | boolean;
bodyParserConfig?: OptionsJson | boolean;
onHealthCheck?: (req: express.Request) => Promise<any>;
disableHealthCheck?: boolean;
}
export interface ServerRegistration extends GetMiddlewareOptions {
app: express.Application;
}
const fileUploadMiddleware = (
uploadsConfig: FileUploadOptions,
server: ApolloServerBase,
) => (
req: express.Request,
res: express.Response,
next: express.NextFunction,
) => {
if (
typeof processFileUploads === 'function' &&
typeis(req, ['multipart/form-data'])
) {
processFileUploads(req, res, uploadsConfig)
.then(body => {
req.body = body;
next();
})
.catch(error => {
if (error.status && error.expose) res.status(error.status);
next(
formatApolloErrors([error], {
formatter: server.requestOptions.formatError,
debug: server.requestOptions.debug,
}),
);
});
} else {
next();
}
};
export interface ExpressContext {
req: express.Request;
res: express.Response;
connection?: ExecutionParams;
}
export interface ApolloServerExpressConfig extends Config {
context?: ContextFunction<ExpressContext, Context> | Context;
}
export class ApolloServer extends ApolloServerBase {
constructor(config: ApolloServerExpressConfig) {
super(config);
}
async createGraphQLServerOptions(
req: express.Request,
res: express.Response,
): Promise<GraphQLOptions> {
return super.graphQLServerOptions({ req, res });
}
public applyMiddleware({ app, ...rest }: ServerRegistration) {
app.use(this.getMiddleware(rest));
}
public getMiddleware({
path,
cors,
bodyParserConfig,
disableHealthCheck,
onHealthCheck,

}: GetMiddlewareOptions = {}): express.Router {
if (!path) path = '/graphql';
const router = express.Router();const promiseWillStart = this.willStart();router.use(path, (_req, _res, next) => {
promiseWillStart.then(() => next()).catch(next);
});
if (!disableHealthCheck) {
router.use('/.well-known/apollo/server-health', (req, res) =>
{
res.type('application/health+json');
if (onHealthCheck) {
onHealthCheck(req)
.then(() => {
res.json({ status: 'pass' });
})
.catch(() => {
res.status(503).json({ status: 'fail' });
});
} else {
res.json({ status: 'pass' });
}
});
}
this.graphqlPath = path;if (cors === true) {
router.use(path, corsMiddleware());
} else if (cors !== false) {
router.use(path, corsMiddleware(cors));
}
if (bodyParserConfig === true) {
router.use(path, json());
} else if (bodyParserConfig !== false) {
router.use(path, json(bodyParserConfig));
}
return graphqlExpress(() => this.createGraphQLServerOptions(req, res))(
req,
res,
next,
);
});
return router;
}
}

If it’s not easy to implement middleware for Apollo GraphQL at the GraphQL level as we do for the Http server, what should we do?

Well, remember my earlier blog about custom Directives and its usage as a field level middleware?

Apart from Directives, we can also use custom Plugins to intercept the request lifecycle, and even add the middlewares directly in the context object that gets passed down to different layers of resolvers such as an Authentication layer.

Below is an example of using context as the authScope. This function is called with every request, so you can set the context based on the request’s details (such as HTTP headers).

Note that the fields of the object passed to your context function differ. In Express, it’s the req, res , for others, see the Doc here.

const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => ({
authScope: getScope(req.headers.authorization)
})
}));
// Example resolver
(parent, args, context, info) => {
if(context.authScope !== ADMIN) throw new AuthenticationError('not admin');
// ...
}

That’s it!

In this blog, we quickly look at the differences in applying middleware to the Express level vs. GraphQL level, and a few alternatives to add middlewares to the GraphQL.

Happy Reading!

--

--