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.
@deprecatedis the only built-in schema directive; all others are custom-built.- Common uses include text transformation (
@upper), access control (@auth), and caching hints (@cacheControl).
