Join us for GraphQL Summit, October 10-12 in San Diego. Use promo code ODYSSEY for $400 off your pass.
Docs
Launch GraphOS Studio
Apollo Server 2 is officially deprecated, with end-of-life scheduled for 22 October 2023. Additionally, certain features are end-of-life as of 31 December 2023. Learn more about these deprecations and upgrading.

Creating schema directives

Apply custom logic to GraphQL types, fields, and arguments


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

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

schema.graphql
# Definition
directive @uppercase on FIELD_DEFINITION
type Query {
# Usage
hello: String @uppercase
}

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

Defining

A definition looks like this:

schema.graphql
directive @deprecated(
reason: String = "No longer supported"
) on FIELD_DEFINITION | ENUM_VALUE
  • This defines a named @deprecated.
  • The takes one optional (reason) with a default value ("No longer supported").
  • The can decorate any number of FIELD_DEFINITIONs and ENUM_VALUEs in your .

Supported locations

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

The table below lists all available locations in a GraphQL . Your 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 within any defined type except an input type (see INPUT_FIELD_DEFINITION)

ARGUMENT_DEFINITION

visitArgumentDefinition(argument: GraphQLArgument)

The definition of a

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 within an input type

SCHEMA

visitSchema(schema: GraphQLSchema)

The top-level schema object declaration with query, mutation, and/or subscription s (this declaration is usually omitted)

Implementing

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

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 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 :

DeprecatedDirective.js
const { SchemaDirectiveVisitor } = require("apollo-server");
// Subclass of SchemaDirectiveVisitor
export class DeprecatedDirective extends SchemaDirectiveVisitor {
// Visitor methods:
// Called when an object field is @deprecated
public visitFieldDefinition(field: GraphQLField<any, any>) {
field.isDeprecated = true;
field.deprecationReason = this.args.reason;
}
// Called when an enum value is @deprecated
public visitEnumValue(value: GraphQLEnumValue) {
value.isDeprecated = true;
value.deprecationReason = this.args.reason;
}
}

This subclass adds two s to the executable '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 's reason argument.

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

index.js
const { ApolloServer, gql } = require("apollo-server");
const { DeprecatedDirective } = require("./DeprecatedDirective");
const typeDefs = gql`
type ExampleType {
newField: String
oldField: String @deprecated(reason: "Use \`newField\`.")
}
`;
const server = new ApolloServer({
typeDefs,
resolvers,
schemaDirectives: {
// Object key must match directive name, minus '@'
deprecated: DeprecatedDirective
}
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});

When Apollo Server parses your 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 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 string into an executable , you can still execute logic by calling the static method SchemaDirectiveVisitor.visitSchemaDirectives. This method takes your executable , along the same form of map you provide to the schemaDirectives constructor option:

SchemaDirectiveVisitor.visitSchemaDirectives(schema, {
deprecated: DeprecatedDirective
});

Transforming resolved fields

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

Example: Uppercasing strings

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

This example defines an @uppercase for this purpose:

Edit upper-case-directive

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

Example: Formatting dates

Suppose your defines a Date custom , 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 for this purpose:

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

Additional examples

Fetching data from a REST API

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

const { ApolloServer, gql, SchemaDirectiveVisitor } = require("apollo-server");
const typeDefs = gql`
directive @rest(url: String) on FIELD_DEFINITION
type Query {
people: [Person] @rest(url: "/api/v1/people")
}
`;
class RestDirective extends SchemaDirectiveVisitor {
public visitFieldDefinition(field) {
const { url } = this.args;
field.resolve = () => fetch(url);
}
}
const server = new ApolloServer({
typeDefs,
schemaDirectives: {
rest: RestDirective
}
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});

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 of a Query type:

const { ApolloServer, gql, SchemaDirectiveVisitor } = require("apollo-server");
const { defaultFieldResolver } = require('graphql');
const typeDefs = gql`
directive @intl on FIELD_DEFINITION
type Query {
greeting: String @intl
}
`;
class IntlDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field, details) {
const { resolve = defaultFieldResolver } = field;
field.resolve = async function (...args) {
const context = args[2];
const defaultText = await resolve.apply(this, args);
// In this example, path would be ["Query", "greeting"]:
const path = [details.objectType.name, field.name];
return translate(defaultText, path, context.locale);
};
}
}
const server = new ApolloServer({
typeDefs,
schemaDirectives: {
intl: IntlDirective
}
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});

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 that takes an requires of type Role, which defaults to ADMIN. This @auth can appear on an OBJECT like User to set default access permissions for all User s, as well as appearing on individual fields, to enforce field-specific @auth restrictions:

directive @auth(
requires: Role = ADMIN,
) on OBJECT | FIELD_DEFINITION
enum Role {
ADMIN
REVIEWER
USER
UNKNOWN
}
type User @auth(requires: USER) {
name: String
banned: Boolean @auth(requires: ADMIN)
canPost: Boolean @auth(requires: REVIEWER)
}

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

const { ApolloServer, gql, SchemaDirectiveVisitor } = require("apollo-server");
const { defaultFieldResolver } = require('graphql');
class AuthDirective extends SchemaDirectiveVisitor {
visitObject(type) {
this.ensureFieldsWrapped(type);
type._requiredAuthRole = this.args.requires;
}
// Visitor methods for nested types like fields and arguments
// also receive a details object that provides information about
// the parent and grandparent types.
visitFieldDefinition(field, details) {
this.ensureFieldsWrapped(details.objectType);
field._requiredAuthRole = this.args.requires;
}
ensureFieldsWrapped(objectType) {
// Mark the GraphQLObjectType object to avoid re-wrapping:
if (objectType._authFieldsWrapped) return;
objectType._authFieldsWrapped = true;
const fields = objectType.getFields();
Object.keys(fields).forEach(fieldName => {
const field = fields[fieldName];
const { resolve = defaultFieldResolver } = field;
field.resolve = async function (...args) {
// Get the required Role from the field first, falling back
// to the objectType if no Role is required by the field:
const requiredRole =
field._requiredAuthRole ||
objectType._requiredAuthRole;
if (! requiredRole) {
return resolve.apply(this, args);
}
const context = args[2];
const user = await getUser(context.headers.authToken);
if (! user.hasRole(requiredRole)) {
throw new Error("not authorized");
}
return resolve.apply(this, args);
};
});
}
}
const server = new ApolloServer({
typeDefs,
schemaDirectives: {
auth: AuthDirective,
authorized: AuthDirective,
authenticated: AuthDirective
}
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});

One drawback of this approach is that it does not guarantee s will be wrapped if they are added to the 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 , 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 :

const { ApolloServer, gql, SchemaDirectiveVisitor } = require('apollo-server');
const { GraphQLScalarType, GraphQLNonNull } = require('graphql');
const typeDefs = gql`
directive @length(max: Int) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
type Query {
books: [Book]
}
type Book {
title: String @length(max: 50)
}
type Mutation {
createBook(book: BookInput): Book
}
input BookInput {
title: String! @length(max: 50)
}
`;
class LengthDirective extends SchemaDirectiveVisitor {
visitInputFieldDefinition(field) {
this.wrapType(field);
}
visitFieldDefinition(field) {
this.wrapType(field);
}
// Replace field.type with a custom GraphQLScalarType that enforces the
// length restriction.
wrapType(field) {
if (
field.type instanceof GraphQLNonNull &&
field.type.ofType instanceof GraphQLScalarType
) {
field.type = new GraphQLNonNull(
new LimitedLengthType(field.type.ofType, this.args.max),
);
} else if (field.type instanceof GraphQLScalarType) {
field.type = new LimitedLengthType(field.type, this.args.max);
} else {
throw new Error(`Not a scalar type: ${field.type}`);
}
}
}
class LimitedLengthType extends GraphQLScalarType {
constructor(type, maxLength) {
super({
name: `LengthAtMost${maxLength}`,
// For more information about GraphQLScalar type (de)serialization,
// see the graphql-js implementation:
// https://github.com/graphql/graphql-js/blob/31ae8a8e8312/src/type/definition.js#L425-L446
serialize(value) {
value = type.serialize(value);
assert.isAtMost(value.length, maxLength);
return value;
},
parseValue(value) {
return type.parseValue(value);
},
parseLiteral(ast) {
return type.parseLiteral(ast);
},
});
}
}
const server = new ApolloServer({
typeDefs,
resolvers,
schemaDirectives: {
length: LengthDirective,
},
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});

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 called uid that combines the with various values to produce an ID that’s unique across your :

const { ApolloServer, gql, SchemaDirectiveVisitor } = require("apollo-server");
const { GraphQLID } = require("graphql");
const { createHash } = require("crypto");
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"]
) on OBJECT
# Since this type just uses the default values of name and from,
# we don't have to pass any arguments to the directive:
type Location @uniqueID {
id: Int
address: String
}
# This type uses both the person's name and the personID field,
# in addition to the "Person" type name, to construct the ID:
type Person @uniqueID(from: ["name", "personID"]) {
personID: Int
name: String
}
`;
class UniqueIdDirective extends SchemaDirectiveVisitor {
visitObject(type) {
const { name, from } = this.args;
const fields = type.getFields();
if (name in fields) {
throw new Error(`Conflicting field name ${name}`);
}
fields[name] = {
name,
type: GraphQLID,
description: 'Unique ID',
args: [],
resolve(object) {
const hash = createHash("sha1");
hash.update(type.name);
from.forEach(fieldName => {
hash.update(String(object[fieldName]));
});
return hash.digest("hex");
}
};
}
}
const server = new ApolloServer({
typeDefs,
resolvers,
schemaDirectives: {
uniqueID: UniqueIdDirective
}
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});

Declaring schema directives

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

directive @auth(
requires: Role = ADMIN,
) on OBJECT | FIELD_DEFINITION
enum Role {
ADMIN
REVIEWER
USER
UNKNOWN
}
type User @auth(requires: USER) {
name: String
banned: Boolean @auth(requires: ADMIN)
canPost: Boolean @auth(requires: REVIEWER)
}

This hypothetical @auth takes an named requires of type Role, which defaults to ADMIN if @auth is used without passing an explicit requires . The @auth can appear on an OBJECT like User to set a default access control for all User s, 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 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 syntax, so you may not have control over which s the author decides to declare, and how. That's why a well-implemented, reusable SchemaDirectiveVisitor should consider overriding the getDirectiveDeclaration method:

const { ApolloServer, gql, SchemaDirectiveVisitor } = require("apollo-server");
const { DirectiveLocation, GraphQLDirective, GraphQLEnumType } = require("graphql");
class AuthDirective extends SchemaDirectiveVisitor {
public visitObject(object: GraphQLObjectType) {...}
public visitFieldDefinition(field: GraphQLField<any, any>) {...}
public static getDirectiveDeclaration(
directiveName: string,
schema: GraphQLSchema,
): GraphQLDirective {
const previousDirective = schema.getDirective(directiveName);
if (previousDirective) {
// If a previous directive declaration exists in the schema, it may be
// better to modify it than to return a new GraphQLDirective object.
previousDirective.args.forEach(arg => {
if (arg.name === 'requires') {
// Lower the default minimum Role from ADMIN to REVIEWER.
arg.defaultValue = 'REVIEWER';
}
});
return previousDirective;
}
// If a previous directive with this name was not found in the schema,
// there are several options:
//
// 1. Construct a new GraphQLDirective (see below).
// 2. Throw an exception to force the client to declare the directive.
// 3. Return null, and forget about declaring this directive.
//
// All three are valid options, since the visitor will still work without
// any declared directives. In fact, unless you're publishing a directive
// implementation for public consumption, you can probably just ignore
// getDirectiveDeclaration altogether.
return new GraphQLDirective({
name: directiveName,
locations: [
DirectiveLocation.OBJECT,
DirectiveLocation.FIELD_DEFINITION,
],
args: {
requires: {
// Having the schema available here is important for obtaining
// references to existing type objects, such as the Role enum.
type: (schema.getType('Role') as GraphQLEnumType),
// Set the default minimum Role to REVIEWER.
defaultValue: 'REVIEWER',
}
}]
});
}
}

Since the getDirectiveDeclaration method receives not only the name of the but also the GraphQLSchema object, it can modify and/or reuse previous declarations found in the , 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 s and permissible locations.

What about query directives?

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

While 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 is more sustainable than burdening your clients with it, though you can probably imagine a similar sort of abstraction for implementing query s. 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.

Previous
Directives
Next
Resolvers
Edit on GitHubEditForumsDiscord