Apollo Server 2 is officially end-of-life as of 22 October 2023.

Learn more about upgrading.

Creating schema directives

Apply custom logic to GraphQL types, fields, and arguments


Before you create a custom schema directive, learn the basics about directives.

Your schema can define custom directives that can then decorate other parts of your schema:

GraphQL
schema.graphql
1# Definition
2directive @uppercase on FIELD_DEFINITION
3
4type Query {
5  # Usage
6  hello: String @uppercase
7}

When Apollo Server loads your schema on startup, it can execute custom logic wherever it encounters a particular directive. For example, it can modify the resolver function for a decorated field (for the schema above, it could transform the hello resolver's original result to uppercase).

Defining

A directive definition looks like this:

GraphQL
schema.graphql
1directive @deprecated(
2  reason: String = "No longer supported"
3) on FIELD_DEFINITION | ENUM_VALUE
  • This defines a directive named @deprecated.

  • The directive takes one optional argument (reason) with a default value ("No longer supported").

  • The directive can decorate any number of FIELD_DEFINITIONs and ENUM_VALUEs in your schema.

Supported locations

Your custom directive can appear only in the schema locations you list after the on keyword in the directive's definition.

The table below lists all available locations in a GraphQL schema. Your directive can support any combination of these locations.

Name /
Visitor Method
Description
SCALAR
visitScalar(scalar: GraphQLScalarType)
The definition of a custom scalar
OBJECT
visitObject(object: GraphQLObjectType)
The definition of an object type
FIELD_DEFINITION
visitFieldDefinition(field: GraphQLField<any, any>)
The definition of a field within any defined type except an input type (see INPUT_FIELD_DEFINITION)
ARGUMENT_DEFINITION
visitArgumentDefinition(argument: GraphQLArgument)
The definition of a field argument
INTERFACE
visitInterface(iface: GraphQLInterfaceType)
The definition of an interface
UNION
visitUnion(union: GraphQLUnionType)
The definition of a union
ENUM
visitEnum(type: GraphQLEnumType)
The definition of an enum
ENUM_VALUE
visitEnumValue(value: GraphQLEnumValue)
The definition of one value within an enum
INPUT_OBJECT
visitInputObject(object: GraphQLInputObjectType)
The definition of an input type
INPUT_FIELD_DEFINITION
visitInputFieldDefinition(field: GraphQLInputField)
The definition of a field within an input type
SCHEMA
visitSchema(schema: GraphQLSchema)
The top-level schema object declaration with query, mutation, and/or subscription fields (this declaration is usually omitted)

Implementing

After you define your directive and its valid locations, you still need to define the logic that Apollo Server executes whenever it encounters the directive in your schema.

To accomplish this, you create a subclass of SchemaDirectiveVisitor, a class that's included in Apollo Server as part of the graphql-tools package.

In your subclass, you override the visitor method for each location your directive can appear in. You can see each location's corresponding visitor method in the table above.

Apollo Server includes graphql-tools version 4. To use another version of the library, see Using a different version of graphql-tools.

Example: @deprecated

Here's a possible implementation of the default @deprecated directive:

JavaScript
DeprecatedDirective.js
1const { SchemaDirectiveVisitor } = require("apollo-server");
2
3// Subclass of SchemaDirectiveVisitor
4export class DeprecatedDirective extends SchemaDirectiveVisitor {
5
6  // Visitor methods:
7
8  // Called when an object field is @deprecated
9  public visitFieldDefinition(field: GraphQLField<any, any>) {
10    field.isDeprecated = true;
11    field.deprecationReason = this.args.reason;
12  }
13
14  // Called when an enum value is @deprecated
15  public visitEnumValue(value: GraphQLEnumValue) {
16    value.isDeprecated = true;
17    value.deprecationReason = this.args.reason;
18  }
19}

This subclass adds two fields to the executable schema's representation of a deprecated field or enum value: a boolean indicating that the item isDeprecated, and a string indicating the deprecationReason. The reason is taken directly from the directive's reason argument.

To add this logic to Apollo Server, you pass the DeprecatedDirective class to the ApolloServer constructor via the schemaDirectives object:

JavaScript
index.js
1const { ApolloServer, gql } = require("apollo-server");
2const { DeprecatedDirective } = require("./DeprecatedDirective");
3
4const typeDefs = gql`
5  type ExampleType {
6    newField: String
7    oldField: String @deprecated(reason: "Use \`newField\`.")
8  }
9`;
10
11const server = new ApolloServer({
12  typeDefs,
13  resolvers,
14  schemaDirectives: {
15    // Object key must match directive name, minus '@'
16    deprecated: DeprecatedDirective
17  }
18});
19
20server.listen().then(({ url }) => {
21  console.log(`🚀 Server ready at ${url}`);
22});

When Apollo Server parses your schema SDL to create your executable schema, it automatically initializes a separate DeprecatedDirective for each instance of @deprecated it encounters. It then calls the appropriate visitor method for the current location.

You can give your SchemaDirectiveVisitor subclass any name. It doesn't need to match the name of the directive it's used for (you can even use the same subclass for multiple directives).

Running directive logic on an executable schema

If Apollo Server has already parsed your SDL string into an executable schema, you can still execute directive logic by calling the static method SchemaDirectiveVisitor.visitSchemaDirectives. This method takes your executable schema, along the same form of directive map you provide to the schemaDirectives constructor option:

JavaScript
1SchemaDirectiveVisitor.visitSchemaDirectives(schema, {
2  deprecated: DeprecatedDirective
3});

Transforming resolved fields

A custom directive can transform a resolved GraphQL field's value before it's returned to the requesting client.

Example: Uppercasing strings

Suppose you want to convert certain String fields in your schema to uppercase before they're returned.

This example defines an @uppercase directive for this purpose:

Click to expand
JavaScript
index.js
1const { ApolloServer, gql, SchemaDirectiveVisitor } = require("apollo-server");
2const { defaultFieldResolver } = require("graphql");
3
4const typeDefs = gql`
5  directive @uppercase on FIELD_DEFINITION
6
7  type Query {
8    hello: String @uppercase
9  }
10`;
11
12const resolvers = {
13  Query: {
14    hello() {
15      return "Hello World!";
16    }
17  }
18};
19
20class UpperCaseDirective extends SchemaDirectiveVisitor {
21
22  // Called on server startup for each @uppercase field
23  visitFieldDefinition(field) {
24
25    // Obtain the field's resolver
26    const { resolve = defaultFieldResolver } = field;
27
28    // *Replace* the field's resolver with a function
29    // that calls the *original* resolver, then converts
30    // the result to uppercase before returning
31    field.resolve = async function (...args) {
32      const result = await resolve.apply(this, args);
33      if (typeof result === "string") {
34        return result.toUpperCase();
35      }
36      return result;
37    };
38  }
39}
40
41const server = new ApolloServer({
42  typeDefs,
43  resolvers,
44  schemaDirectives: {
45    uppercase: UpperCaseDirective,
46  }
47});
48
49server.listen().then(({ url }) => {
50  console.log(`🚀 Server ready at ${url}`);
51});
Edit upper-case-directive

This code replaces the resolver of an @uppercase field with a new function. This new function first calls the original resolver, then transforms its result to uppercase (assuming it's a string) before returning it.

Example: Formatting dates

Suppose your schema defines a Date custom scalar, and you want querying clients to be able to specify a string format for the returned date (e.g., specify dd/mm/yyyy HH:MM:ss for a string like 10/03/2021 14:53:03).

This example defines a @date directive for this purpose:

Click to expand
JavaScript
1const { ApolloServer, gql, SchemaDirectiveVisitor } = require('apollo-server');
2const { defaultFieldResolver } = require('graphql');
3const formatDate = require('dateformat');
4
5const typeDefs = gql`
6
7  # Define the directive with a default date format
8  directive @date(
9    defaultFormat: String = "dd/mm/yyyy HH:MM:ss"
10  ) on FIELD_DEFINITION
11
12  scalar Date
13
14  type Post {
15    # You can override the defaultFormat for individual fields
16    published: Date @date(defaultFormat: "dd/mm/yyyy")
17  }
18`;
19
20class FormattableDateDirective extends SchemaDirectiveVisitor {
21  public visitFieldDefinition(field) {
22
23    // Get the field's resolver
24    const { resolve = defaultFieldResolver } = field;
25
26    // Get the default date format
27    const { defaultFormat } = this.args;
28
29    // *Add* a `format` argument to the GraphQL field for
30    // clients to use
31    field.args.push({
32      name: 'format',
33      type: GraphQLString
34    });
35
36    // *Replace* the field's resolver with this function, which
37    // first calls the *original* resolver, then formats the
38    // returned date
39    field.resolve = async function (
40      source,
41      { format, ...otherArgs },
42      context,
43      info,
44    ) {
45      const date = await resolve.call(this, source, otherArgs, context, info);
46      // If the client does not specify a format, use defaultFormat
47      return formatDate(date, format || defaultFormat);
48    };
49
50    // The field now returns a String instead of a Date.
51    // Update it accordingly.
52    field.type = GraphQLString;
53  }
54}
55
56const server = new ApolloServer({
57  typeDefs,
58  schemaDirectives: {
59    date: FormattableDateDirective
60  }
61});
62
63server.listen().then(({ url }) => {
64  console.log(`🚀 Server ready at ${url}`);
65});

Now the client can specify a desired format argument when requesting the Post.published field, or omit the argument to use the defaultFormat string specified in the schema.

Additional examples

Fetching data from a REST API

Suppose you've defined an object type that corresponds to a REST resource, and you want to avoid implementing resolver functions for every field:

JavaScript
1const { ApolloServer, gql, SchemaDirectiveVisitor } = require("apollo-server");
2
3const typeDefs = gql`
4  directive @rest(url: String) on FIELD_DEFINITION
5
6  type Query {
7    people: [Person] @rest(url: "/api/v1/people")
8  }
9`;
10
11class RestDirective extends SchemaDirectiveVisitor {
12  public visitFieldDefinition(field) {
13    const { url } = this.args;
14    field.resolve = () => fetch(url);
15  }
16}
17
18const server = new ApolloServer({
19  typeDefs,
20  schemaDirectives: {
21    rest: RestDirective
22  }
23});
24
25server.listen().then(({ url }) => {
26  console.log(`🚀 Server ready at ${url}`);
27});

There are many more issues to consider when implementing a real GraphQL wrapper over a REST endpoint (such as how to do caching or pagination), but this example demonstrates the basic structure.

Marking strings for internationalization

Suppose you have a function called translate that takes a string, a path identifying that string's role in your application, and a target locale for the translation.

Here's how you might make sure translate is used to localize the greeting field of a Query type:

JavaScript
1const { ApolloServer, gql, SchemaDirectiveVisitor } = require("apollo-server");
2const { defaultFieldResolver } = require('graphql');
3
4const typeDefs = gql`
5  directive @intl on FIELD_DEFINITION
6
7  type Query {
8    greeting: String @intl
9  }
10`;
11
12class IntlDirective extends SchemaDirectiveVisitor {
13  visitFieldDefinition(field, details) {
14    const { resolve = defaultFieldResolver } = field;
15    field.resolve = async function (...args) {
16      const context = args[2];
17      const defaultText = await resolve.apply(this, args);
18      // In this example, path would be ["Query", "greeting"]:
19      const path = [details.objectType.name, field.name];
20      return translate(defaultText, path, context.locale);
21    };
22  }
23}
24
25const server = new ApolloServer({
26  typeDefs,
27  schemaDirectives: {
28    intl: IntlDirective
29  }
30});
31
32server.listen().then(({ url }) => {
33  console.log(`🚀 Server ready at ${url}`);
34});

GraphQL is great for internationalization, since a GraphQL server can access unlimited translation data, and clients can simply ask for the translations they need.

Enforcing access permissions

Imagine a hypothetical @auth directive that takes an argument requires of type Role, which defaults to ADMIN. This @auth directive can appear on an OBJECT like User to set default access permissions for all User fields, as well as appearing on individual fields, to enforce field-specific @auth restrictions:

GraphQL
1directive @auth(
2  requires: Role = ADMIN,
3) on OBJECT | FIELD_DEFINITION
4
5enum Role {
6  ADMIN
7  REVIEWER
8  USER
9  UNKNOWN
10}
11
12type User @auth(requires: USER) {
13  name: String
14  banned: Boolean @auth(requires: ADMIN)
15  canPost: Boolean @auth(requires: REVIEWER)
16}

What makes this example tricky is that the OBJECT version of the directive needs to wrap all fields of the object, even though some of those fields may be individually wrapped by @auth directives at the FIELD_DEFINITION level, and we would prefer not to rewrap resolvers if we can help it:

JavaScript
1const { ApolloServer, gql, SchemaDirectiveVisitor } = require("apollo-server");
2const { defaultFieldResolver } = require('graphql');
3
4class AuthDirective extends SchemaDirectiveVisitor {
5  visitObject(type) {
6    this.ensureFieldsWrapped(type);
7    type._requiredAuthRole = this.args.requires;
8  }
9  // Visitor methods for nested types like fields and arguments
10  // also receive a details object that provides information about
11  // the parent and grandparent types.
12  visitFieldDefinition(field, details) {
13    this.ensureFieldsWrapped(details.objectType);
14    field._requiredAuthRole = this.args.requires;
15  }
16
17  ensureFieldsWrapped(objectType) {
18    // Mark the GraphQLObjectType object to avoid re-wrapping:
19    if (objectType._authFieldsWrapped) return;
20    objectType._authFieldsWrapped = true;
21
22    const fields = objectType.getFields();
23
24    Object.keys(fields).forEach(fieldName => {
25      const field = fields[fieldName];
26      const { resolve = defaultFieldResolver } = field;
27      field.resolve = async function (...args) {
28        // Get the required Role from the field first, falling back
29        // to the objectType if no Role is required by the field:
30        const requiredRole =
31          field._requiredAuthRole ||
32          objectType._requiredAuthRole;
33
34        if (! requiredRole) {
35          return resolve.apply(this, args);
36        }
37
38        const context = args[2];
39        const user = await getUser(context.headers.authToken);
40        if (! user.hasRole(requiredRole)) {
41          throw new Error("not authorized");
42        }
43
44        return resolve.apply(this, args);
45      };
46    });
47  }
48}
49
50const server = new ApolloServer({
51  typeDefs,
52  schemaDirectives: {
53    auth: AuthDirective,
54    authorized: AuthDirective,
55    authenticated: AuthDirective
56  }
57});
58
59server.listen().then(({ url }) => {
60  console.log(`🚀 Server ready at ${url}`);
61});

One drawback of this approach is that it does not guarantee fields will be wrapped if they are added to the schema after AuthDirective is applied, and the whole getUser(context.headers.authToken) is a made-up API that would need to be fleshed out. In other words, we’ve glossed over some of the details that would be required for a production-ready implementation of this directive, though we hope the basic structure shown here inspires you to find clever solutions to the remaining problems.

Enforcing value restrictions

Suppose you want to enforce a maximum length for a string-valued field:

JavaScript
1const { ApolloServer, gql, SchemaDirectiveVisitor } = require('apollo-server');
2const { GraphQLScalarType, GraphQLNonNull } = require('graphql');
3
4const typeDefs = gql`
5  directive @length(max: Int) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
6
7  type Query {
8    books: [Book]
9  }
10
11  type Book {
12    title: String @length(max: 50)
13  }
14
15  type Mutation {
16    createBook(book: BookInput): Book
17  }
18
19  input BookInput {
20    title: String! @length(max: 50)
21  }
22`;
23
24class LengthDirective extends SchemaDirectiveVisitor {
25  visitInputFieldDefinition(field) {
26    this.wrapType(field);
27  }
28
29  visitFieldDefinition(field) {
30    this.wrapType(field);
31  }
32
33  // Replace field.type with a custom GraphQLScalarType that enforces the
34  // length restriction.
35  wrapType(field) {
36    if (
37      field.type instanceof GraphQLNonNull &&
38      field.type.ofType instanceof GraphQLScalarType
39    ) {
40      field.type = new GraphQLNonNull(
41        new LimitedLengthType(field.type.ofType, this.args.max),
42      );
43    } else if (field.type instanceof GraphQLScalarType) {
44      field.type = new LimitedLengthType(field.type, this.args.max);
45    } else {
46      throw new Error(`Not a scalar type: ${field.type}`);
47    }
48  }
49}
50
51class LimitedLengthType extends GraphQLScalarType {
52  constructor(type, maxLength) {
53    super({
54      name: `LengthAtMost${maxLength}`,
55
56      // For more information about GraphQLScalar type (de)serialization,
57      // see the graphql-js implementation:
58      // https://github.com/graphql/graphql-js/blob/31ae8a8e8312/src/type/definition.js#L425-L446
59
60      serialize(value) {
61        value = type.serialize(value);
62        assert.isAtMost(value.length, maxLength);
63        return value;
64      },
65
66      parseValue(value) {
67        return type.parseValue(value);
68      },
69
70      parseLiteral(ast) {
71        return type.parseLiteral(ast);
72      },
73    });
74  }
75}
76
77const server = new ApolloServer({
78  typeDefs,
79  resolvers,
80  schemaDirectives: {
81    length: LengthDirective,
82  },
83});
84
85server.listen().then(({ url }) => {
86  console.log(`🚀 Server ready at ${url}`);
87});

Synthesizing unique IDs

Suppose your database uses incrementing IDs for each resource type, so IDs are not unique across all resource types. Here’s how you might synthesize a field called uid that combines the object type with various field values to produce an ID that’s unique across your schema:

JavaScript
1const { ApolloServer, gql, SchemaDirectiveVisitor } = require("apollo-server");
2const { GraphQLID } = require("graphql");
3const { createHash } = require("crypto");
4
5const typeDefs = gql`
6  directive @uniqueID(
7    # The name of the new ID field, "uid" by default:
8    name: String = "uid"
9
10    # Which fields to include in the new ID:
11    from: [String] = ["id"]
12  ) on OBJECT
13
14  # Since this type just uses the default values of name and from,
15  # we don't have to pass any arguments to the directive:
16  type Location @uniqueID {
17    id: Int
18    address: String
19  }
20
21  # This type uses both the person's name and the personID field,
22  # in addition to the "Person" type name, to construct the ID:
23  type Person @uniqueID(from: ["name", "personID"]) {
24    personID: Int
25    name: String
26  }
27`;
28
29class UniqueIdDirective extends SchemaDirectiveVisitor {
30  visitObject(type) {
31    const { name, from } = this.args;
32    const fields = type.getFields();
33    if (name in fields) {
34      throw new Error(`Conflicting field name ${name}`);
35    }
36    fields[name] = {
37      name,
38      type: GraphQLID,
39      description: 'Unique ID',
40      args: [],
41      resolve(object) {
42        const hash = createHash("sha1");
43        hash.update(type.name);
44        from.forEach(fieldName => {
45          hash.update(String(object[fieldName]));
46        });
47        return hash.digest("hex");
48      }
49    };
50  }
51}
52
53const server = new ApolloServer({
54  typeDefs,
55  resolvers,
56  schemaDirectives: {
57    uniqueID: UniqueIdDirective
58  }
59});
60
61server.listen().then(({ url }) => {
62  console.log(`🚀 Server ready at ${url}`);
63});

Declaring schema directives

While the above examples should be sufficient to implement any @directive used in your schema, SDL syntax also supports declaring the names, argument types, default argument values, and permissible locations of any available directives:

JavaScript
1directive @auth(
2  requires: Role = ADMIN,
3) on OBJECT | FIELD_DEFINITION
4
5enum Role {
6  ADMIN
7  REVIEWER
8  USER
9  UNKNOWN
10}
11
12type User @auth(requires: USER) {
13  name: String
14  banned: Boolean @auth(requires: ADMIN)
15  canPost: Boolean @auth(requires: REVIEWER)
16}

This hypothetical @auth directive takes an argument named requires of type Role, which defaults to ADMIN if @auth is used without passing an explicit requires argument. The @auth directive can appear on an OBJECT like User to set a default access control for all User fields, and also on individual fields, to enforce field-specific @auth restrictions.

Enforcing the requirements of the declaration is something a SchemaDirectiveVisitor implementation could do itself, in theory, but the SDL syntax is easer to read and write, and provides value even if you're not using the SchemaDirectiveVisitor abstraction.

However, if you're implementing a reusable SchemaDirectiveVisitor for public consumption, you will probably not be the person writing the SDL syntax, so you may not have control over which directives the schema author decides to declare, and how. That's why a well-implemented, reusable SchemaDirectiveVisitor should consider overriding the getDirectiveDeclaration method:

JavaScript
1const { ApolloServer, gql, SchemaDirectiveVisitor } = require("apollo-server");
2const { DirectiveLocation, GraphQLDirective, GraphQLEnumType } = require("graphql");
3
4class AuthDirective extends SchemaDirectiveVisitor {
5  public visitObject(object: GraphQLObjectType) {...}
6  public visitFieldDefinition(field: GraphQLField<any, any>) {...}
7
8  public static getDirectiveDeclaration(
9    directiveName: string,
10    schema: GraphQLSchema,
11  ): GraphQLDirective {
12    const previousDirective = schema.getDirective(directiveName);
13    if (previousDirective) {
14      // If a previous directive declaration exists in the schema, it may be
15      // better to modify it than to return a new GraphQLDirective object.
16      previousDirective.args.forEach(arg => {
17        if (arg.name === 'requires') {
18          // Lower the default minimum Role from ADMIN to REVIEWER.
19          arg.defaultValue = 'REVIEWER';
20        }
21      });
22
23      return previousDirective;
24    }
25
26    // If a previous directive with this name was not found in the schema,
27    // there are several options:
28    //
29    // 1. Construct a new GraphQLDirective (see below).
30    // 2. Throw an exception to force the client to declare the directive.
31    // 3. Return null, and forget about declaring this directive.
32    //
33    // All three are valid options, since the visitor will still work without
34    // any declared directives. In fact, unless you're publishing a directive
35    // implementation for public consumption, you can probably just ignore
36    // getDirectiveDeclaration altogether.
37
38    return new GraphQLDirective({
39      name: directiveName,
40      locations: [
41        DirectiveLocation.OBJECT,
42        DirectiveLocation.FIELD_DEFINITION,
43      ],
44      args: {
45        requires: {
46          // Having the schema available here is important for obtaining
47          // references to existing type objects, such as the Role enum.
48          type: (schema.getType('Role') as GraphQLEnumType),
49          // Set the default minimum Role to REVIEWER.
50          defaultValue: 'REVIEWER',
51        }
52      }]
53    });
54  }
55}

Since the getDirectiveDeclaration method receives not only the name of the directive but also the GraphQLSchema object, it can modify and/or reuse previous declarations found in the schema, as an alternative to returning a totally new GraphQLDirective object. Either way, if the visitor returns a non-null GraphQLDirective from getDirectiveDeclaration, that declaration will be used to check arguments and permissible locations.

What about query directives?

As its name suggests, the SchemaDirectiveVisitor abstraction is specifically designed to enable transforming GraphQL schemas based on directives that appear in your SDL text.

While directive syntax can also appear in GraphQL queries sent from the client, implementing query directives would require runtime transformation of query documents. We have deliberately restricted this implementation to transformations that take place at server construction time.

We believe confining this logic to your schema is more sustainable than burdening your clients with it, though you can probably imagine a similar sort of abstraction for implementing query directives. If that possibility becomes a desire that becomes a need for you, let us know, and we may consider supporting query directives in a future version of these tools.

Feedback

Edit on GitHub

Forums