GraphQL Custom Directives

Custom directives let you add your own logic that runs as part of schema processing or field resolution. They act as decorators you attach to fields, types, or arguments in the SDL. Use them for cross-cutting concerns — formatting, access control, deprecation, rate limiting — that would otherwise clutter every resolver.

Two Kinds of Custom Directives

  Schema directives                    Execution directives
  (SDL decoration)                     (query-time behavior)
  ─────────────────                    ────────────────────
  Attached to types and fields         Attached to fields in queries
  Run at server startup                Run per request
  Examples: @deprecated,               Examples: @include, @skip,
  @auth, @format, @upper               custom query-time logic

Declaring a Custom Directive in SDL

  # Declare the directive and where it can be used
  directive @upper on FIELD_DEFINITION

  type Product {
    id:   ID!
    name: String! @upper    ← Apply it to the name field
  }

Implementing @upper with mapSchema

Declaring a directive in SDL does nothing on its own. You must implement it with a transformer function that modifies field resolvers. The @graphql-tools/utils package provides the mapSchema utility for this.

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

  import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';
  import { makeExecutableSchema } from '@graphql-tools/schema';

  function upperDirectiveTransformer(schema, directiveName) {
    return mapSchema(schema, {
      [MapperKind.OBJECT_FIELD]: (fieldConfig) => {

        const directive = getDirective(schema, fieldConfig, directiveName)?.[0];
        if (!directive) return fieldConfig;   // No directive on this field

        const { resolve = defaultFieldResolver } = fieldConfig;

        // Wrap the original resolver
        fieldConfig.resolve = async (source, args, context, info) => {
          const result = await resolve(source, args, context, info);
          if (typeof result === 'string') {
            return result.toUpperCase();      // Transform the value
          }
          return result;
        };
        return fieldConfig;
      }
    });
  }

  // Build the schema then apply the transformer
  let schema = makeExecutableSchema({ typeDefs, resolvers });
  schema = upperDirectiveTransformer(schema, 'upper');

@deprecated — Built-In Schema Directive

GraphQL ships with @deprecated as a built-in schema directive. It marks a field or enum value as outdated and shows the deprecation message in tools like Apollo Sandbox.

  type User {
    id:       ID!
    name:     String!
    username: String @deprecated(reason: "Use name instead")
  }

  Apollo Sandbox shows:
  ─────────────────────
  username  [DEPRECATED] Use name instead

@auth Directive Pattern

A common use of custom directives is protecting fields with an authorization check. The directive wraps the resolver and throws an error before any data is fetched if the user lacks permission.

  directive @auth(requires: Role = USER) on FIELD_DEFINITION

  enum Role { GUEST USER ADMIN }

  type Query {
    publicData:  String
    myProfile:   User    @auth(requires: USER)
    adminReport: Report  @auth(requires: ADMIN)
  }

  // Transformer checks context.user.role before resolving
  if (context.user.role !== requiredRole) {
    throw new Error('Forbidden');
  }

Directive Locations

  Location keyword         Where you can attach the directive
  ────────────────         ──────────────────────────────────
  FIELD_DEFINITION         Fields on object types in SDL
  OBJECT                   Object type definitions
  ARGUMENT_DEFINITION      Arguments on fields
  ENUM_VALUE               Individual values inside an enum
  FIELD                    Fields in a query (client-side)
  FRAGMENT_SPREAD          ...FragmentName in a query
  INLINE_FRAGMENT          ... on Type in a query

Key Points

  • Custom directives act as decorators that add reusable logic to schema fields without touching every resolver.
  • Declaring a directive in SDL is just metadata — you must implement a transformer to give it behavior.
  • @deprecated is the only built-in schema directive; all others are custom-built.
  • Common uses include text transformation (@upper), access control (@auth), and caching hints (@cacheControl).

Leave a Comment