Apollo GraphQL Custom Directives

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

Photo by Eaters Collective on Unsplash

This is a 2nd 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.

According to the Doc: a directive decorates part of a GraphQL schema or operation with additional configuration.

A directive is an identifier preceded by a @ character, optionally followed by a list of named arguments.

directive @deprecated(
reason: String = "No longer supported"
type ExampleType {
newField: String
oldField: String @deprecated(reason: "Use `newField`.")

There are 3 default directives specified in the GraphQL specification, @skip, @include, and @ deprecated.That means, if you are implementing your own version of GraphQL, you also need to have these 3 directives.

There are two types of directives:

  • Schema directives, which are defined in the SDL and executed when building the schema
  • Operation directives, which appear in the query and are executed when resolving the query

Using the default directives as examples:

  • @deprecated is a schema type directive used to mark a field as deprecated in the SDL, and can only be added to FIELD_DEFINITION or ENUM_VALUE locations (more on this below).
  • @skip and @include are operation type directives to denote whether or not to execute a section of the query at runtime. And can only be added to FIELD, FRAGMENT_SPREAD, or INLINE_FRAGMENT (more on this below).

Whether or not the directive is Schema or Operation one depends on its location. Instead, it uses directive locations to define where a directive is used.There are two groups of directive locations: executable directive locations (i.e., operation type) and type system directive location (i.e., schema type).


Note that Schema type directives are private. They are implemented inside the server and not exposed to the end user who uses the client. Operation types are public as they can be used by the end user when implementing queries over the client.

In real life, schema type directives are more popular, as it make little sense for client to have extensive operation type directives to modify their queries, they can easily do this before they construct the query. Even if they implement this, the runtime transformation is more costly and error prone comparing to transformation at schema compile time.

If you look at the examples on the doc, almost all of them are schema type based: data fetching, formatting, authorisation, caching, validation, as well as the analytics/logging/metrics.

Schema Directives are extremely powerful tool to enhance the capabilities of your SDL without adding too much burden to your resolvers.The very common ones are those implemented on FIELD_DEFINITION or OBJECT .

The typical workflow for any schema type directive (field level, e.g.) is to take the field value resolved by the field resolver as input, do its job, and output the result to the next directive on the pipeline or return directly if no directive danach.

query {
field1 @directive1 @directive2 @directive3

Isn’t this look familiar?

It is essentially a pipeline! Or thinking bigger, it is a middleware pipeline!

You may ask that the typical middleware is a duplex stream, so how do we fix that?

Well, in that sense, you can put any directive you’d like it to proceed the data again like so:

query {
field1 @directive1 @directive2 @directive3 @directive2 @directive1

To implement a schema directive using SchemaDirectiveVisitor, simply create a subclass of SchemaDirectiveVisitor that overrides one or more of the following visitor methods:

  • visitSchema(schema: GraphQLSchema)
  • visitScalar(scalar: GraphQLScalarType)
  • visitObject(object: GraphQLObjectType)
  • visitFieldDefinition(field: GraphQLField<any, any>)
  • visitArgumentDefinition(argument: GraphQLArgument)
  • visitInterface(iface: GraphQLInterfaceType)
  • visitUnion(union: GraphQLUnionType)
  • visitEnum(type: GraphQLEnumType)
  • visitEnumValue(value: GraphQLEnumValue)
  • visitInputObject(object: GraphQLInputObjectType)
  • visitInputFieldDefinition(field: GraphQLInputField)

By overriding methods like visitObject, a subclass of SchemaDirectiveVisitor expresses interest in certain schema types such as GraphQLObjectType .

Here is one possible implementation of the @deprecated directive we saw above:

const { SchemaDirectiveVisitor } = require("apollo-server");class DeprecatedDirective extends SchemaDirectiveVisitor {
public visitFieldDefinition(field: GraphQLField<any, any>) {
field.isDeprecated = true;
field.deprecationReason = this.args.reason;
public visitEnumValue(value: GraphQLEnumValue) {
value.isDeprecated = true;
value.deprecationReason = this.args.reason;
/===================================================================const { ApolloServer, gql } = require("apollo-server");const typeDefs = gql`
type ExampleType {
newField: String
oldField: String @deprecated(reason: "Use \`newField\`.")
const server = new ApolloServer({
schemaDirectives: {
deprecated: DeprecatedDirective
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);

I’d recommend to checkout the examples on the docs as they are comprehensive enough to provide a starting point for writing your own custom directives.

But here are some other examples to use as a reference (in TypeScript):


import { SchemaDirectiveVisitor } from "apollo-server";
import { defaultFieldResolver, GraphQLString } from "graphql";
import DateFormatter from "dateformat";

const defaultFormat = "mmmm d, yyyy";
export class DateFormatDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
// Extracting the default resolver function:
const { resolve = defaultFieldResolver } = field;
// Assign default format to as instance property to the class
const { format = defaultFormat } = this.args;
// The "args" below are essentially [parent, args, root, info] //that passed to resolver field
field.resolve = async function (...args) {
const date = await resolve.apply(this, args);
return DateFormatter(date, format);
field.type = GraphQLString;
export const schemaDirectives = {
date: DateFormatDirective
==================const typeDefs = gql`
directive @date(format: String) on FIELD_DEFINITION
type Capsule {
original_launch: Date @date(format: "HH:MM")


import { SchemaDirectiveVisitor } from "apollo-server";
import {
} from "graphql";
import {
} from "lodash";

export type LodashTransform = (arg: string) => string;

const StringDirectiveFactory = (
directiveName: string,
transform: LodashTransform
): typeof SchemaDirectiveVisitor => {
class StringDirective extends SchemaDirectiveVisitor { static getDirectiveDeclaration(
directiveName: string,
schema: GraphQLSchema
): GraphQLDirective {
// if currentDirective already exist do nothing
const currentDirective = schema.getDirective(directiveName);
if (currentDirective) {
return currentDirective;

// if not, then create a new one as requested
return new GraphQLDirective({
name: directiveName,
locations: [DirectiveLocation.FIELD_DEFINITION],
description: `string directive to manupulate ${this.name}`,
isRepeatable: true, // can be placed multiple times

visitFieldDefinition(field: GraphQLField<any, any>) {
const { resolve = defaultFieldResolver } = field;
field.resolve = async function (...args) {
const result = await resolve.apply(this, args);
if (typeof result === "string") {
return transform(result);
return result;
return StringDirective;

export const UpperStringDirective = StringDirectiveFactory("upper", upperCase);
export const LowerStringDirective = StringDirectiveFactory("lower", lowerCase);
export const CamelCaseStringDirective = StringDirectiveFactory(
export const DeburrStringDirective = StringDirectiveFactory("deburr", deburr);
export const CapitalizeStringDirective = StringDirectiveFactory(
export const KebabCaseStringDirective = StringDirectiveFactory(
export const TrimStringDirective = StringDirectiveFactory("trim", trim);
export const SnakeCaseStringDirective = StringDirectiveFactory(

Note that the getDirectiveDeclaration above is very useful when you need to expose your Directives publicly such as in a npm package, and you would like to control what name of the directives the user decides to declare, and how.


import { SchemaDirectiveVisitor } from "apollo-server";
import { GraphQLID, GraphQLObjectType } from "graphql";
import { createHash } from "crypto";

export class UniqueIdDirective extends SchemaDirectiveVisitor {
visitObject(type: GraphQLObjectType) {
// Extracting name, and from argument from the directive
const { name, from } = this.args;
// Get all fields on this object type
const fields = type.getFields();
if (name in fields) {
// Throw error if there is a field with the same name exists
throw new Error(`Conflicting field name ${name}`);
fields[name] = {
type: GraphQLID,
description: "Unique ID",
args: [],
resolve(object) {
const hash = createHash("sha1");
from.forEach((fieldName) => {
return hash.digest("hex");
isDeprecated: false,
extensions: {},
export const schemaDirectives = {
uniqueID: UniqueIdDirective
const typeDefs = gql`
directive @uniqueID(
# The name of the new ID field, "uid" by DEFAULT
name: String = "uid"
# Which fields to include in the new ID:
from: [String] = ["id"]
type Capsule @uniqueID {
id: ID
landings: Int
type: String

When querying the Capsule type on the client end or in GraphQL playground on the server, you will see a newly generated uid field based on id field value through the directive.


We will use the custom scalars created in the last blog to generate limitation directives:

import { SchemaDirectiveVisitor } from "apollo-server";
import {
} from "graphql";
import { MaxLength, MinLength, GreaterThan, LessThan } from "../scalars";

export interface CustomScalarClass<T = any> {
new (...args: any[]): T;

const LimitDirectiveFactory = (
ScalarType: CustomScalarClass
): typeof SchemaDirectiveVisitor => {
class LimitDirective extends SchemaDirectiveVisitor {
field: GraphQLInputField
): GraphQLInputField | void | null {

field: GraphQLField<any, any, any>
): GraphQLField<any, any> | void | null {

wrapType(field: GraphQLField<any, any, any> | GraphQLInputField) {
if (
field.type instanceof GraphQLNonNull &&
field.type.ofType instanceof GraphQLScalarType
/* field type is a custom scalar whose ofType resolve to a scalar */
) {
field.type = new GraphQLNonNull(
new ScalarType(field.type.ofType, this.args["limit"])
} else if (
field.type instanceof GraphQLScalarType
/* field type itself is scalar, e.g. Int */
) {
// the args here is the directive arges, e.g. { limit: -1 }
field.type = new ScalarType(field.type, this.args["limit"]);
} else {
throw new Error(`Not a scalar type: ${field.type}`);

return LimitDirective;

export const MaxLengthDirective = LimitDirectiveFactory(MaxLength);export const MinLengthDirective = LimitDirectiveFactory(MinLength);
export const GreaterThanDirective = LimitDirectiveFactory(GreaterThan);
export const LessThanDirective = LimitDirectiveFactory(LessThan);
export const schemaDirectives = {
greater: GreaterThanDirective,
less: LessThanDirective,
max: MaxLengthDirective,
min: MinLengthDirective,
const typeDefs = gql`
directive @greater(limit: Int) on
type Capsule {
id: ID
crew_capacity: Int #@greater(limit: 3)
type: String

Note that it is totally fine to place multiple directives in one field, like a middleware chain, or even using one directive multiple times:

type Capsule @cacheControl(maxAge: 6000) @uniqueID {
id: ID
landings: Int
reuse_count: Int
status: String
type: String

Lastly, check out the source code here if you want to know more about directives.

Ok, that’s so much of it!

Happy Reading!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store