Apollo Server 3 is officially end-of-life as of 22 October 2024.

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}

See an example implementation of this directive on CodeSandbox:

Edit upper-case-directive

When you start up your app, you can use directives to transform your executable schema's behavior before you provide that schema to Apollo Server. For example, you can modify the resolver function for any 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 is the definition for the @deprecated directive in the GraphQL spec.

  • 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 /
MapperKind
Description
SCALAR
SCALAR_TYPE
The definition of a custom scalar
OBJECT
OBJECT_TYPE
The definition of an object type
FIELD_DEFINITION
OBJECT_FIELD
The definition of a field within any defined type except an input type (see INPUT_FIELD_DEFINITION)
ARGUMENT_DEFINITION
ARGUMENT
The definition of a field argument
INTERFACE
INTERFACE_TYPE
The definition of an interface
UNION
UNION_TYPE
The definition of a union
ENUM
ENUM_TYPE
The definition of an enum
ENUM_VALUE
ENUM_VALUE
The definition of one value within an enum
INPUT_OBJECT
INPUT_OBJECT_TYPE
The definition of an input type
INPUT_FIELD_DEFINITION
INPUT_OBJECT_FIELD
The definition of a field within an input type
SCHEMA
ROOT_OBJECT
The top-level schema object declaration with query, mutation, and/or subscription fields (this declaration is usually omitted)

Implementing

Important: Apollo Server 3 does not provide built-in support for custom directives. To enable this support, you need to install certain @graphql-tools libraries.

This article uses @graphql-tools version 8. Previous versions use a different API for custom directives. If you're using an earlier version of @graphql-tools, see the Apollo Server v2 docs.

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 define custom directive logic with @graphql-tools v8 and later, you can create transformer functions that transform an executable schema's behavior based on the directives that are present in it.

1. Install required libraries

First, install the following @graphql-tools libraries:

Bash
1npm install @graphql-tools/schema @graphql-tools/utils

2. Define directive logic

To define what Apollo Server does when it encounters your directive, you can create a transformer function. This function uses the mapSchema function to iterate through locations in your schema (field definitions, type definitions, etc.) and perform transformations wherever it encounters a particular directive (or set of directives).

For example, here's a possible transformer function for the default @deprecated directive:

JavaScript
directives.js
1const { mapSchema, getDirective, MapperKind } = require('@graphql-tools/utils');
2
3function deprecatedDirectiveTransformer(schema, directiveName) {
4  return  mapSchema(schema, {
5
6    // Executes once for each object field definition in the schema
7    [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
8      const deprecatedDirective = getDirective(schema, fieldConfig, directiveName)?.[0];
9      if (deprecatedDirective) {
10        fieldConfig.deprecationReason = deprecatedDirective['reason'];
11        return fieldConfig;
12      }
13    },
14
15    // Executes once for each enum value definition in the schema
16    [MapperKind.ENUM_VALUE]: (enumValueConfig) => {
17      const deprecatedDirective = getDirective(schema, enumValueConfig, directiveName)?.[0];
18      if (deprecatedDirective) {
19        enumValueConfig.deprecationReason = deprecatedDirective['reason'];
20        return enumValueConfig;
21      }
22    }
23  });
24};

As shown, the second parameter you pass mapSchema is an object with keys that represent one or more locations in your schema. The MapperKind enum value for each supported location is listed in the table above.

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 } = require('apollo-server');
2const {
3  ApolloServerPluginLandingPageLocalDefault,
4} = require('apollo-server-core');
5const { makeExecutableSchema } = require('@graphql-tools/schema');
6const { mapSchema, getDirective, MapperKind } = require('@graphql-tools/utils');
7const { defaultFieldResolver } = require('graphql');
8
9// Our GraphQL schema
10const typeDefs = gql`
11  directive @upper on FIELD_DEFINITION
12
13  type Query {
14    hello: String @upper
15  }
16`;
17
18// Our resolvers (notice the hard-coded string is *not* all-caps)
19const resolvers = {
20  Query: {
21    hello() {
22      return 'Hello World!';
23    }
24  }
25};
26
27// This function takes in a schema and adds upper-casing logic
28// to every resolver for an object field that has a directive with
29// the specified name (we're using `upper`)
30function upperDirectiveTransformer(schema, directiveName) {
31  return mapSchema(schema, {
32
33    // Executes once for each object field in the schema
34    [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
35
36      // Check whether this field has the specified directive
37      const upperDirective = getDirective(schema, fieldConfig, directiveName)?.[0];
38
39      if (upperDirective) {
40
41        // Get this field's original resolver
42        const { resolve = defaultFieldResolver } = fieldConfig;
43
44        // Replace the original resolver with a function that *first* calls
45        // the original resolver, then converts its result to upper case
46        fieldConfig.resolve = async function (source, args, context, info) {
47          const result = await resolve(source, args, context, info);
48          if (typeof result === 'string') {
49            return result.toUpperCase();
50          }
51          return result;
52        }
53        return fieldConfig;
54      }
55    }
56  });
57}
58
59// Create the base executable schema
60let schema = makeExecutableSchema({
61  typeDefs,
62  resolvers
63});
64
65// Transform the schema by applying directive logic
66schema = upperDirectiveTransformer(schema, 'upper');
67
68// Provide the schema to the ApolloServer constructor
69const server = new ApolloServer({
70  schema,
71  csrfPrevention: true,
72  cache: 'bounded',
73  plugins: [
74    ApolloServerPluginLandingPageLocalDefault({ embed: true }),
75  ],
76});
77
78server.listen().then(({ url }) => {
79  console.log(`🚀 Server ready at ${url}`);
80});

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.

Additional examples

For additional examples of transforming executable schemas with directives and mapSchema, see the @graphql-tools docs.

What about query directives?

Although directive syntax can also appear in GraphQL queries sent from the client, implementing query directives requires 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 need for you, let us know.

Feedback

Edit on GitHub

Forums