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.  

Loading comments...
You've successfully subscribed to @Tech.hebdo
Great! Next, complete checkout to get full access to all premium content.
Error! Could not sign up. invalid link.
Welcome back! You've successfully signed in.
Error! Could not sign in. Please try again.
Success! Your account is fully activated, you now have access to all content.
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.