Improve your Apollo server with Apollo Server Plugins.
As covered in other post on this blog, Apollo is a platform that allows you to build scalable and robust web/Mobile application by providing you everything you need for client and server when using GraphQL, a query language for data fetching. The image bellow briefly describe it.
In this blog post, we will work on the server part and will focus how to improve a Node.js GraphQL server by adding some customs features using Plugins.
1. What are plugins
In computer science and software development, plugins are software additions that helps to customize programs, apps, and web browsers.
For Apollo, Plugins extend Apollo Server's functionality by performing custom operations in response to certain events. These events correspond to individual phases of the GraphQL request lifecycle, and to the lifecycle of Apollo Server itself.
A basic example can be one that perform an action before server start. To do this, we will override the serverWillStart
event.
const myPlugin = {
async serverWillStart() {
console.log('Server starting up!');
},
};
when there are well used, plugins can behave like middleware where you perform some actions before or after certain events.
To understand how plugins works and how we can create customs ones, it is really important to understand GraphQL request lifecycle methods.
The following diagram illustrates the sequence of events that fire for each request. Each of these events is documented in Apollo Server plugin events and available on apollo website.
So you will be able to create plugins that perform some actions for each of theses specifics operations. To learn more about each of theses methods(), you can read more here.
2. Setup
For this implementation, we will use a ready to use node.js graphql server we built on another post. So we will fetch the source code that run a graphQL server with mocked data and JWT configured for authorization.
Since the directory containing the initial setup is part of a repository that host multiples tutorials source code each on a single folder, we will only clone the one with the initial setup we need to get start. Follow the folling steps from the terminal.
git clone --no-checkout https://github.com/Doha26/fullstack-hebdo-posts.git
cd fullstack-hebdo-posts
git sparse-checkout init --cone
git sparse-checkout set 04-nodejs-graphql-apollo-plugins
And now inside the folder, install node dependencies and start the server
cd 04-nodejs-graphql-apollo-plugins && yarn && yarn start:dev
you will see the following output
3. Create apollo plugin
From the terminal, install apollo-server-plugin-base
with yarn add apollo-server-plugin-base
This lib will give us access to some Object required for graphql server Lifecycle Event methods
Create a folder called utils
inside src
folder and add a file called plugin.ts
Inside this file, add the following code to create a Custom Apollo Server plugin.
import { GraphQLSchema } from 'graphql'
import {
ApolloServerPlugin,
BaseContext,
GraphQLRequestContext,
GraphQLRequestExecutionListener,
GraphQLRequestListener,
GraphQLRequestListenerParsingDidEnd,
GraphQLRequestListenerValidationDidEnd,
GraphQLResponse
} from 'apollo-server-plugin-base'
export class CustomApolloServerPlugin implements ApolloServerPlugin {
public serverWillStart(): any {
console.info('- Inside serverWillStart')
}
public requestDidStart(): Promise<GraphQLRequestListener<BaseContext>> | any {
const start = Date.now()
let variables: any = null
return {
didResolveOperation: (): Promise<void> | any => {
console.info('- Inside didResolveOperation')
},
willSendResponse(requestContext: GraphQLRequestContext) {
const stop = Date.now()
const elapsed = stop - start
const size = JSON.stringify(requestContext.response).length * 2
console.info(
`operation=${requestContext.operationName},
duration=${elapsed}ms,
bytes=${size},
query=${requestContext.request.query}
variables:${JSON.stringify(variables)},
user=${JSON.stringify(requestContext.context.user)}
responseData=${JSON.stringify(requestContext.response?.data)},
`
)
},
didEncounterErrors(requestContext: GraphQLRequestContext) {}
}
}
/**
* Request Lifecycle Handlers
*/
public parsingDidStart(context: GraphQLRequestContext): Promise<GraphQLRequestListenerParsingDidEnd | void> | any {
console.info('- Inside parsingDidStart', JSON.stringify(context))
}
public validationDidStart(
context: GraphQLRequestContext
): Promise<GraphQLRequestListenerValidationDidEnd | void> | any {
console.info('- Inside validationDidStart', JSON.stringify(context))
}
public didResolveOperation(context: GraphQLRequestContext): Promise<void> | any {
console.info('- Inside didResolveOperation', JSON.stringify(context))
}
public responseForOperation(context: GraphQLRequestContext): GraphQLResponse | any {
console.info('- Inside responseForOperation', JSON.stringify(context))
return null
}
public executionDidStart(context: GraphQLRequestContext): Promise<GraphQLRequestExecutionListener | void> | any {
console.info('- Inside executionDidStart', JSON.stringify(context))
}
public didEncounterErrors(context: GraphQLRequestContext): Promise<void> | any {
console.info('- Inside didEncounterErrors', JSON.stringify(context))
}
public willSendResponse(context: GraphQLRequestContext): Promise<void> | any {
console.info('- Inside willSendResponse', JSON.stringify(context))
}
}
Once done, open src/index.ts
file and add the following to enable the plugin.
const server = new ApolloServer({
schema,
context: async ({ req, res }): Promise<AppContext | null> => {
// ...
},
plugins: [new CustomApolloServerPlugin()] // NEW
})
And that's all. Now if the server is running, you should be able to see the following ouput from the console
which refer to this in our plugin class
4. Let's explain some of theses lifecycle methods and actions that can be handle for each
For better understanding on how it can be useful to configure a Custom Apollo plugin, let's explain (According to apollo) what is behind the scenes of theses methods.
- serverWillStart(): This method is triggered when te apollo server is ready to start. At this level, it would be interesting to make sure that all your production secret keys (.env VARIABLES ) have been loaded. Otherwise, raise an error
- requestDidStart(): Triggered at the beginning of every request cycle, and returns an object (GraphQLRequestListener) that has the functions for each request lifecycle event (didResolveOperation(), willSendResponse(), etc ..)
- didResolveOperation(context: GraphQLRequestContext): This event fires after the graphql library successfully determines the operation to execute from a request's document AST (Abstract Syntax Tree). At this stage, both the operationName string and operation AST are available. the @param context argument expose following items.
metrics
|source
|document
|operationName
|operation
- willSendResponse(requestContext: GraphQLRequestContext): The event fires whenever Apollo Server is about to send a response for a GraphQL operation. This event fires (and Apollo Server sends a response) even if the GraphQL operation encounters one or more errors. The @param context argument provide following items:
metrics
|response
- didEncounterErrors(requestContext: GraphQLRequestContext): The didEncounterErrors event fires when Apollo Server encounters errors while parsing, validating, or executing a GraphQL operation. The @param context argument provide following items:
metrics
|source
|errors
. Here could be the good place to send the error to your favorite error tracking platform (Bugsnag, sentry, firebase, etc ...). We will see how to do this in another blog post.
- parsingDidStart(context: GraphQLRequestContext): This event fires whenever Apollo Server will parse a GraphQL request to create its associated document AST (Abstract Syntax Tree). 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 not called for the request, because parsing does not occur. The @param context argument provide following items:
metrics
|source
- validationDidStart(context: GraphQLRequestContext ): This event fires whenever Apollo Server will validate a request's document AST against your GraphQL schema. Like parsingDidStart, this event does not fire if a request's document is already available in Apollo Server's cache. (only successfully validated documents are cached by Apollo Server). The document AST is guaranteed to be available at this stage, because parsing must succeed for validation to occur. The @param context argument provide following items:
metrics
|source
|document
- responseForOperation(context: GraphQLRequestContext): This 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. The @param context argument provide following items:
metrics
|source
|document
|operationName
|operation
- executionDidStart(context: GraphQLRequestContext): This event fires whenever Apollo Server begins executing the GraphQL operation specified by a request's document AST. The @param context argument provide following items:
metrics
|source
|document
|operationName
|operation
Thanks for reading, the source code is available here.
Follow me on socials media to stay up to date with latest posts.