GraphQL Custom Directives — Building Reusable Schema Annotations and Middleware
In this tutorial, you will learn about Graphql Custom Directives. We cover key concepts, practical examples, and best practices to help you master this topic.
GraphQL custom directives allow you to define reusable annotations that modify schema behavior at the field, object, or operation level, enabling cross-cutting concerns without boilerplate.
What You'll Learn
- Defining and implementing custom directives
- Directive location types and restrictions
- Common directive use cases
Why It Matters
Custom directives encapsulate reusable behavior. Without them, authorization, Caching, and validation logic is duplicated across resolvers. DodaTech uses custom directives for auth and logging across all GraphQL services.
Code Examples
# Custom directive definitions
directive @auth(
requires: Role = ADMIN
) on OBJECT | FIELD_DEFINITION
directive @rateLimit(
max: Int!
window: Int!
) on FIELD_DEFINITION
directive @log(
level: String = "info"
includeArgs: Boolean = false
) on FIELD_DEFINITION
directive @uppercase on FIELD_DEFINITION
directive @truncate(maxLength: Int!) on FIELD_DEFINITION
// Apollo Server custom directive implementation
const { SchemaDirectiveVisitor } = require('apollo-server');
class RateLimitDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { max, window } = this.args;
const originalResolve = field.resolve;
field.resolve = async function (parent, args, context, info) {
const key = `${context.user?.id || 'anonymous'}:${info.fieldName}`;
const current = await redis.get(key) || 0;
if (current >= max) {
throw new Error(`Rate limit exceeded. Max ${max} per ${window}s`);
}
await redis.incr(key);
await redis.expire(key, window);
return originalResolve.call(this, parent, args, context, info);
};
}
}
const schema = makeExecutableSchema({
typeDefs,
schemaDirectives: {
rateLimit: RateLimitDirective,
auth: AuthDirective,
log: LogDirective
}
});
# Python custom directive with Strawberry
import strawberry
from strawberry.directives import Directive
@strawberry.directive(
locations=[DirectiveLocation.FIELD_DEFINITION]
)
def uppercase(value: str) -> str:
return value.upper()
@strawberry.directive(
locations=[DirectiveLocation.FIELD_DEFINITION]
)
def truncate(value: str, max_length: int) -> str:
if len(value) > max_length:
return value[:max_length] + '...'
return value
Common Mistakes
1. Making Directives Too Broad
Directives should have specific locations. Avoid using OBJECT when FIELD_DEFINITION is sufficient.
2. Not Handling Directive Execution Order
Multiple directives on the same field execute in a specific order. Document the expected order.
3. Creating Directives That Mutate Arguments
Directives should not modify request arguments unexpectedly. Document side effects.
4. Ignoring Directive Performance
Directives that make database calls or API requests add latency to every field resolution.
5. Not Providing Default Values
Directive arguments should have sensible defaults where possible to simplify usage.
Practice Questions
- How do you define a custom directive?
- What is a directive location?
- How do directives affect field resolution?
- Can multiple directives be applied to the same field?
- How do you pass arguments to directives?
Answers:
- With the directive keyword: directive @name(arg: Type) on Location.
- The schema element where the directive can be applied (FIELD_DEFINITION, OBJECT, QUERY, etc.).
- Directives wrap the original resolver with custom logic before or after execution.
- Yes, multiple directives can be applied. Execution order depends on the implementation.
- Arguments are defined in parentheses after the directive name.
Challenge: Build a reusable directives library with @auth, @rateLimit, @log, @validate, and @cacheControl directives. Implement each as a class and compose multiple directives on the same field.
FAQ
Mini Project
Create a comprehensive directives library for a production GraphQL API. Implement @auth with role hierarchy, @rateLimit with Redis backend, @log with structured logging, @validate with field constraints, and @transform for data formatting. Include unit tests.
What's Next
Explore nested queries and resolver chains, then learn about resolver arguments in detail.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro