Apollo GraphQL Plugin All in One

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

E.Y.
9 min readApr 28, 2021
Photo by Jonatan Pie on Unsplash

This is a 3rd 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.

In last blog we dig into custom Directive and its usage as field level middleware. In this blog, we are looking at Plugins.

Plugin🔧 🔨 ⚒ 🛠 ⛏ 🔩 ⚙ ⛓ ?

What are plugins? First, you may ask.

Quoting the doc, plugins enable you to extend Apollo Server’s core functionality by performing custom operations in response to certain events. Currently, these events correspond to individual phases of the GraphQL request lifecycle, and to the startup of Apollo Server itself.

So basically, plugins are actions you can fire across different stages from the start to the end of the Apollo server/request lifecycle, and is event driven.

Note: You can think of Request lifecyle is nested under Server lifecycle, as obviously, you can’t fire a request when the server hasn’t started.

Apollo Server Event Reference

Apollo Server fires two types of events that plugins can hook into: server lifecycle events and request lifecycle events.

Server lifecycle events are high-level events related to the lifecycle of Apollo Server itself: serverWillStart and requestDidStart. Request lifecycle events are defined within the response to a requestDidStart event, as the diagram shows below:

Note: Any event below that can result in “Success” can also result in an error. Whenever an error occurs, the didEncounterErrors event fires and the remainder of the "Success" path stops.

https://www.apollographql.com/docs/apollo-server/integrations/plugins/#request-lifecycle-events

In the graph above, requestDidStart and request lifecycle functions accept a requestContext parameter of type GraphQLRequestContext, which includes a request (type GraphQLRequest), along with a response field ( type GraphQLResponse) if it's available.

Deep Dive into Different Events

  • Server lifecyle:

serverWillStart

The serverWillStart event fires when Apollo Server is preparing to start serving GraphQL requests. It helps you prepare other dependencies are available before begin serving requests.

serverWillStop

The serverWillStop event fires when Apollo Server is starting to shut down when ApolloServer.stop() is invoked . You can gracefully shutdown other services e.g. disconnect database.

It is defined in the object returned by serverWillStart handler, because the two handlers usually interact with the same data. Just like the cleanup effect you see in React useEffect hook.

const server = new ApolloServer({
plugins: [
{
serverWillStart() {
const interval = setInterval(doSomethingPeriodically, 1000);
return {
serverWillStop() {
clearInterval(interval);
}
}
}
}
]
})

requestDidStart

The requestDidStart fires whenever Apollo Server receives a GraphQL request. It optionally return an object that includes functions for responding to request lifecycle events shown in the diagram above.

const myPlugin = {
requestDidStart(requestContext) {
console.log('Request started!');
return {
parsingDidStart(requestContext) {
console.log('Parsing started!');
},
validationDidStart(requestContext) {
console.log('Validation started!');
}
}
},
};
  • Request lifecycle events

didResolveSource

The didResolveSource event is invoked after Apollo Server has determined the String-representation of the incoming operation that it will act upon. Note that at this stage, there is not a guarantee that the operation is not malformed.

parsingDidStart

The parsingDidStart event fires whenever Apollo Server will parse a GraphQL request to create its associated document AST. For caching purpose, if Apollo Server receives a request with a query string that matches a previous request, the associated document might already be available in Apollo Server's cache. In this case, parsingDidStart is skipped.

validationDidStart

The validationDidStart event fires whenever Apollo Server will validate a request's document AST against GraphQL schema. This event is also skipped if a request's document is already available in Apollo Server's cache.

didResolveOperation

The didResolveOperation event fires after the graphql library successfully determines the operation to execute from a request's document AST. At this stage, both the operationName string and operation AST are available. Note that if the operation is anonymous, then operationName is null.

responseForOperation

The responseForOperation event is fired immediately before GraphQL execution would take place. If its return value resolves to a non-null GraphQLResponse, that result is used instead of executing the query. Hooks from different plugins are invoked in series, and the first non-null response is used.

executionDidStart

The executionDidStart event fires whenever Apollo Server begins executing the GraphQL operation specified by a request's document AST. executionDidStart may return an "end hook" function. Alternatively, it may return an object with one or both of the methods executionDidEnd and willResolveField. executionDidEnd is treated identically to an end hook: it is called after execution with any errors that occurred.

willResolveField

The willResolveField event fires whenever Apollo Server is about to resolve a single field during the execution of an operation. The handler is passed an object with four fields (source, args, context, and info) that correspond to the four positional arguments passed to resolvers. You provide your willResolveField handler in the object returned by your executionDidStart handler. Your willResolveField handler can optionally return an "end hook".

const server = new ApolloServer({
plugins: [
{
requestDidStart(initialRequestContext) {
return {
executionDidStart(executionRequestContext) {
return {
willResolveField({source, args, context, info}) {
const start = process.hrtime.bigint();
return (error, result) => {
const end = process.hrtime.bigint();
console.log(`Field ${info.parentType.name}.${info.fieldName} took ${end - start}ns`);
if (error) {
console.log(`It failed with ${error}`);
} else {
console.log(`It returned ${result}`);
}
};
}
}
}
}
}
}
]
})

didEncounterErrors

The didEncounterErrors event fires when Apollo Server encounters errors while parsing, validating, or executing a GraphQL operation.

willSendResponse

The willSendResponse event fires whenever Apollo Server is about to send a response for a GraphQL operation. This event fires even if the GraphQL operation encounters one or more errors.

  • End hooks

Event handlers for the following events can optionally return a function that is invoked after the corresponding lifecycle phase ends:

  • parsingDidStart
  • validationDidStart
  • executionDidStart
  • willResolveField

These end hooks are passed any errors that occurred during the execution of that lifecycle phase. For example, the following plugin logs any errors that occur during any of the above lifecycle events:

const myPlugin = {
requestDidStart() {
return {
parsingDidStart() {
return (err) => {
if (err) {
console.error(err);
}
}
},
validationDidStart() {
// This end hook is unique in that it can receive an array
// of errors which contain all validation errors.
return (errs) => {
if (errs) {
errs.forEach(err => console.error(err));
}
}
},
executionDidStart() {
return (err) => {
if (err) {
console.error(err);
}
}
}
}
}
}

The willResolveField end hook receives the error thrown by the resolver as the first argument and the result of the resolver as the second argument.

You can take a look at the type definitions for these methods here: (Source)

export interface ApolloServerPlugin<
TContext extends BaseContext = BaseContext
> {
serverWillStart?(
service: GraphQLServiceContext,
): ValueOrPromise<GraphQLServerListener | void>;
requestDidStart?(
requestContext: GraphQLRequestContext<TContext>,
): GraphQLRequestListener<TContext> | void;
}
interface GraphQLRequest {
query?: string;
operationName?: string;
variables?: { [name: string]: any };
extensions?: Record<string, any>;
http?: Pick<Request, 'url' | 'method' | 'headers'>;
}
interface GraphQLResponse {
data?: Record<string, any> | null;
errors?: ReadonlyArray<GraphQLFormattedError>;
extensions?: Record<string, any>;
http?: Pick<Response, 'headers'> & Partial<Pick<Mutable<Response>, 'status'>>;
}
interface GraphQLRequestContext<TContext = Record<string, any>> {
readonly request: GraphQLRequest;
readonly response?: GraphQLResponse;
logger: Logger;
readonly schema: GraphQLSchema;
readonly schemaHash: SchemaHash;
readonly context: TContext;
readonly cache: KeyValueCache;
readonly queryHash?: string;
readonly document?: DocumentNode;
readonly source?: string;
readonly operationName?: string | null;
readonly operation?: OperationDefinitionNode;
readonly errors?: ReadonlyArray<GraphQLError>;
readonly metrics: GraphQLRequestMetrics;
debug?: boolean;
}
interface GraphQLRequestMetrics {
captureTraces?: boolean;
persistedQueryHit?: boolean;
persistedQueryRegister?: boolean;
responseCacheHit?: boolean;
forbiddenOperation?: boolean;
registeredOperation?: boolean;
startHrTime?: [number, number];
queryPlanTrace?: Trace.QueryPlanNode;
}
type GraphQLRequestContextDidResolveSource<TContext> =
WithRequired<GraphQLRequestContext<TContext>,
| 'metrics'
| 'source'
| 'queryHash'
>;
type GraphQLRequestContextParsingDidStart<TContext> =
GraphQLRequestContextDidResolveSource<TContext>;
export type GraphQLRequestContextValidationDidStart<TContext> =
GraphQLRequestContextParsingDidStart<TContext> &
WithRequired<GraphQLRequestContext<TContext>,
| 'document'
>;
type GraphQLRequestContextDidResolveOperation<TContext> =
GraphQLRequestContextValidationDidStart<TContext> &
WithRequired<GraphQLRequestContext<TContext>,
| 'operation'
| 'operationName'
>;
type GraphQLRequestContextDidEncounterErrors<TContext> =
WithRequired<GraphQLRequestContext<TContext>,
| 'metrics'
| 'errors'
>;
type GraphQLRequestContextResponseForOperation<TContext> =
WithRequired<GraphQLRequestContext<TContext>,
| 'metrics'
| 'source'
| 'document'
| 'operation'
| 'operationName'
>;
type GraphQLRequestContextExecutionDidStart<TContext> =
GraphQLRequestContextParsingDidStart<TContext> &
WithRequired<GraphQLRequestContext<TContext>,
| 'document'
| 'operation'
| 'operationName'
>;
type GraphQLRequestContextWillSendResponse<TContext> =
GraphQLRequestContextDidResolveSource<TContext> &
WithRequired<GraphQLRequestContext<TContext>,
| 'metrics'
| 'response'
>;

Plugins or Extensions?

In some older versions of Apollo GraphQL Server package, you may see that extensions is used instead of plugins . Essentially they are the same thing but of a bit different interface.

interface GraphQLServerOptions<
TContext = Record<string, any>,
TRootValue = any
> {
extensions?: Array<() => GraphQLExtension>;
plugins?: ApolloServerPlugin[];
}

You can also find the definition of extension in /graphql-extensions/src/index.ts , which is essentially a class interface corresponding to `ApolloServerPlugin .

Builtin Plugins

There are some builtin plugins by Apollo:

Custom Plugins

The good thing about Plugins is that you can set up fine-grained operation in response to individual phases of the GraphQL request lifecycle. To define a plugin is not difficult as long as it satisfies the shape of `ApolloServerPlugin interface or a factory function that returns `ApolloServerPlugin .

export type PluginDefinition = ApolloServerPlugin | (() => ApolloServerPlugin);

Let’s look at a few examples.

Complexity Plugin:

import { GraphQLSchema, separateOperations } from "graphql";
import { PluginDefinition } from "apollo-server-core";
import {
GraphQLRequestContext,
ApolloServerPlugin,
GraphQLServiceContext,
GraphQLServerListener,
} from "apollo-server-plugin-base";
import {
getComplexity,
simpleEstimator,
fieldExtensionsEstimator,
} from "graphql-query-complexity";
import { ValueOrPromise } from "apollo-server-types";

const MAX_ALLOWED_COMPLEXITY = 100;

const ComplexityPlugin: ApolloServerPlugin = {
requestDidStart(requestContext) {
return {
didResolveOperation: ({ request, document, schema }) => {
const complexity = getComplexity({
schema: schema,
query: request.operationName
? separateOperations(document)[request.operationName]
: document,
variables: request.variables,
estimators: [
fieldExtensionsEstimator(),
simpleEstimator({ defaultComplexity: 1 }),
],
});
if (complexity > this.maxAllowedComplexity) {
throw new Error(
`Sorry, too complicated query (complexity: ${complexity}, max complexity: ${this.maxAllowedComplexity})`
);
}
},
};
},
};

export default ComplexityPlugin;

Here we are using a package graphql-query-complexity to calculate the query complexity. We plugin in to the didResolveOperation stage where we can get the operation that executes a request’s document AST. And if the query is too complexity, we return an error.

ErrorHandler Plugin:

import type { ApolloServerPlugin } from "apollo-server-plugin-base";
import { Logger, LOG_TYPE } from "../utils/logger";
const logger = new Logger();

const ErrorHandlerPlugin: ApolloServerPlugin = {
requestDidStart({ queryHash, operationName }) {
return {
didEncounterErrors: ({ errors }) =>
errors.forEach((error) => {
logger.log(LOG_TYPE.ERROR, "Error encountered", {
queryHash,
operationName,
error,
});
}),
};
},
};

export default ErrorHandlerPlugin;

This ErrorPlugin will be triggered whenever Apollo Server encounters errors while parsing, validating, or executing a GraphQL operation. It simply add the error to an external logger.

Logger Plugin:

import type { ApolloServerPlugin } from "apollo-server-plugin-base";
import { Logger, LOG_TYPE } from "../utils/logger";

const logger = new Logger();
export const LoggerPlugin: ApolloServerPlugin = {
serverWillStart() {
logger.log(LOG_TYPE.INFO, "Server is starting");
},
requestDidStart(requestContext) {
const {
request: { operationName, variables },
} = requestContext
;
logger.log(LOG_TYPE.DATA, "Request start", {
operationName: operationName,
variables: variables ?? false,
});

return {
willSendResponse: ({ errors }) => {
logger.log(LOG_TYPE.DATA, "Request end", {
errors: !!errors,
//instead of print it we just need to know if error exist
});
},
};
},
};

export default LoggerPlugin;

This is an extension of ErrorLogger from the earlier example. So instead of plugging into only one request cycle event, the logger logs at serverWillStart, requestDidStart and just before a response is sent back on willSendResponse .

Note here we are grabbing some objects from the requestContext that is passed down into requestDidStart . The objects available vary at different stage, e.g. you can get the the document at ParsingDidStart, but not at didResolveSource .

Lastly, here is the template to use if you’d like to have a go :

/* eslint-disable @typescript-eslint/no-empty-function */
import {
ApolloServerPlugin,
GraphQLRequestContext,
GraphQLRequestContextDidEncounterErrors,
GraphQLRequestContextDidResolveOperation,
GraphQLRequestContextDidResolveSource,
GraphQLRequestContextExecutionDidStart,
GraphQLRequestContextParsingDidStart,
GraphQLRequestContextResponseForOperation,
GraphQLRequestContextValidationDidStart,
GraphQLRequestContextWillSendResponse,
GraphQLServiceContext,
} from "apollo-server-plugin-base";

type BaseContext = Record<string, any>;

const templatePlugin: ApolloServerPlugin<BaseContext> = {
async serverWillStart(serviceContext: GraphQLServiceContext) {
return {
serverWillStop: async () => {},
};
},

requestDidStart(requestContext: GraphQLRequestContext<BaseContext>) {
return {
didResolveSource: async (
requsetContext: GraphQLRequestContextDidResolveSource<BaseContext>
) => {},
parsingDidStart: (
requsetContext: GraphQLRequestContextParsingDidStart<BaseContext>
) => {},
validationDidStart: (
requsetContext: GraphQLRequestContextValidationDidStart<BaseContext>
) => {},
didResolveOperation: async (
requsetContext: GraphQLRequestContextDidResolveOperation<BaseContext>
) => {},
didEncounterErrors: async (
requsetContext: GraphQLRequestContextDidEncounterErrors<BaseContext>
) => {},
responseForOperation: async (
requsetContext: GraphQLRequestContextResponseForOperation<BaseContext>
) => null,
executionDidStart: (
requsetContext: GraphQLRequestContextExecutionDidStart<BaseContext>
) => {},
willSendResponse: async (
requsetContext: GraphQLRequestContextWillSendResponse<BaseContext>
) => {},
};
},
};

export default templatePlugin;

That’s all for today!

Happy Reading :)!

--

--

No responses yet