Practical examples of using directives in GraphQL (with real patterns)
Real-world examples of using directives in GraphQL
Let’s start with concrete examples of using directives in GraphQL the way teams actually use them in production. The spec only defines a small set of built-in directives, but modern servers let you define your own and wire them into your execution pipeline.
You’ll see these patterns across public APIs, internal platforms, and federated graphs:
- Conditional field fetching and A/B tests
- Access control and multi-tenant rules
- Rate limiting and throttling
- Caching hints for CDNs and gateways
- Field masking for privacy and compliance
- Schema evolution and deprecation strategies
Each example of directive usage here is written in SDL (schema definition language) with a Node/TypeScript flavor, but the ideas translate cleanly to Java, Go, Python, or Rust GraphQL servers.
Classic examples of using directives in GraphQL: @include and @skip
The best examples to warm up with are the two directives that ship with the GraphQL spec: @include and @skip. They live on the query side, not in the schema.
query UserProfile($withPosts: Boolean!) {
me {
id
name
email
posts @include(if: $withPosts) {
id
title
}
}
}
Here, @include controls whether the posts field is resolved. On the flip side, @skip does the opposite:
query AdminView($hideSensitive: Boolean!) {
user(id: "123") {
id
name
email @skip(if: $hideSensitive)
}
}
These are simple examples of examples of using directives in GraphQL to keep a single query flexible across multiple UI states. Instead of maintaining separate queries for “full profile” and “light profile,” you toggle behavior with variables.
In large React or Next.js apps, this pattern helps keep the number of persisted queries manageable, especially when paired with clients like Apollo or Relay.
Schema-level example of a custom @auth directive
Now for the pattern everyone cares about: authorization. One of the best examples of using directives in GraphQL is a custom @auth directive that encodes access rules directly in the schema.
Defining the directive in SDL
directive @auth(
requires: Role! = USER
) on FIELD_DEFINITION | OBJECT
enum Role {
USER
ADMIN
}
type User @auth(requires: USER) {
id: ID!
email: String! @auth(requires: ADMIN)
name: String!
}
This example of @auth applies a default rule to the whole User type, then overrides it on the email field. The directive is metadata; the server wiring decides what to actually do with it.
Wiring @auth in a Node/TypeScript server (Apollo-style)
import { defaultFieldResolver, GraphQLField } from 'graphql';
function authDirectiveTransformer(schema, directiveName = 'auth') {
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig: GraphQLField<any, any>) => {
const authDirective = getDirective(schema, fieldConfig, directiveName)?.[0];
if (!authDirective) return fieldConfig;
const { requires } = authDirective;
const originalResolve = fieldConfig.resolve || defaultFieldResolver;
fieldConfig.resolve = async (source, args, context, info) => {
if (!context.user || !context.user.roles?.includes(requires)) {
throw new Error('Not authorized');
}
return originalResolve(source, args, context, info);
};
return fieldConfig;
},
});
}
This is one of the best examples of using directives in GraphQL to keep auth rules close to the schema instead of scattering checks across resolvers. In 2024–2025, you see this pattern a lot in multi-tenant SaaS platforms where roles and scopes change faster than the schema.
Feature flags and experiments: @feature and @variant
Product teams love flags; backend teams hate tangled if-statements. Directives give you a middle ground.
Feature flag directive example
directive @feature(
flag: String!
) on FIELD_DEFINITION
type Query {
me: User
experimentalDashboard: Dashboard @feature(flag: "dash_v2")
}
At runtime, your directive logic can consult LaunchDarkly, Optimizely, or a homegrown flag service. Only if the flag is on for the current user does the resolver run.
This is one of the more modern examples of examples of using directives in GraphQL, especially in organizations moving toward progressive delivery. Instead of branching your schema, you keep a single schema and let directives control exposure.
A/B testing with @variant
directive @variant(
experiment: String!
bucket: String!
) on FIELD_DEFINITION
type Query {
pricingPage: PricingPage
pricingExperiment: PricingPage
@variant(experiment: "pricing_copy", bucket: "B")
}
Resolvers can use the directive metadata to log experiment exposure or to switch underlying data sources.
Caching hints: @cacheControl and edge-aware directives
Caching is another area where examples of using directives in GraphQL really shine. Apollo’s @cacheControl is a well-known pattern:
directive @cacheControl(
maxAge: Int
scope: CacheControlScope
) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION
enum CacheControlScope {
PUBLIC
PRIVATE
}
type Product @cacheControl(maxAge: 300, scope: PUBLIC) {
id: ID!
name: String!
price: Float!
inventory: Int @cacheControl(maxAge: 30)
}
This is a clean example of how directives can feed HTTP headers and CDN behavior without hard-coding TTLs in resolvers. In 2024–2025, with CDNs and edge networks (Cloudflare Workers, Vercel Edge, Fastly Compute) everywhere, this pattern is getting more sophisticated:
directive @edgeCache(
maxAge: Int!
staleWhileRevalidate: Int = 0
) on FIELD_DEFINITION | OBJECT
type Query {
trendingPosts: [Post!]!
@edgeCache(maxAge: 60, staleWhileRevalidate: 300)
}
Your server can translate @edgeCache into Cache-Control headers that modern CDNs understand, or into surrogate keys for cache invalidation.
Rate limiting and abuse protection with @rateLimit
If your GraphQL API is public-facing, you eventually need to throttle bad behavior. Another strong example of using directives in GraphQL is a @rateLimit directive.
directive @rateLimit(
max: Int!
window: String! # e.g. "1m", "1h"
key: RateLimitKey! = IP
) on FIELD_DEFINITION
enum RateLimitKey {
IP
USER
API_KEY
}
type Query {
search(term: String!): [Result!]!
@rateLimit(max: 30, window: "1m", key: IP)
}
Your directive logic can plug into Redis, Memcached, or a managed store to track counts. Because the limits live in the schema, ops teams can audit and adjust them without digging through resolver code.
This is one of the best examples of examples of using directives in GraphQL to express operational policy declaratively.
For more background on rate limiting patterns and abuse prevention, the general API security guidance from NIST is a worthwhile reference: https://csrc.nist.gov
Privacy, PII, and compliance: @sensitive and @mask
With GDPR, CCPA, and a patchwork of state laws in the U.S., field-level privacy rules are no longer a nice-to-have. Directives give you a schema-native way to mark sensitive data.
Marking sensitive fields
directive @sensitive(
pii: Boolean = true
audit: Boolean = true
) on FIELD_DEFINITION
type User {
id: ID!
name: String!
email: String! @sensitive
ssnLast4: String @sensitive(pii: true, audit: true)
}
Masking output with @mask
directive @mask(
strategy: MaskStrategy! = PARTIAL
) on FIELD_DEFINITION
enum MaskStrategy {
FULL
PARTIAL
}
type PaymentMethod {
id: ID!
cardNumber: String! @mask(strategy: PARTIAL)
}
Resolver middleware can read @sensitive and @mask and either redact values, log access, or enforce stricter auth.
If you’re working in healthcare or life sciences, you’ll also want to align these patterns with HIPAA and PHI guidance. General references from the U.S. Department of Health & Human Services and NIH are useful starting points:
- https://www.hhs.gov/hipaa/index.html
- https://www.nih.gov
These are real examples of using directives in GraphQL to keep compliance logic centralized instead of buried in dozens of resolvers.
Schema evolution: @deprecated and custom @version
You already know @deprecated, but it’s worth calling out as one of the best examples of using directives in GraphQL because it shows how tooling can understand directives.
type User {
username: String! @deprecated(reason: "Use handle instead")
handle: String!
}
Clients like Apollo and Relay can surface this in IDEs and codegen. For more control, some teams add a custom @version directive:
directive @version(
introduced: String!
sunset: String
) on FIELD_DEFINITION | OBJECT
type Query {
legacySearch: [Result!]!
@version(introduced: "2021-01-01", sunset: "2025-06-30")
}
Your schema registry or CI checks can read @version and warn when sunset dates are near. In large organizations, this becomes one of the more practical examples of examples of using directives in GraphQL to keep long-lived graphs from accumulating endless legacy fields.
Federated graphs and gateway-specific directives
If you’re working with Apollo Federation, you already live in a world of directives: @key, @provides, @requires, @external, and friends. These are textbook examples of using directives in GraphQL to coordinate behavior across multiple services.
type Product @key(fields: "id") {
id: ID!
name: String!
}
extend type Product @key(fields: "id") {
id: ID! @external
reviews: [Review!]! @requires(fields: "id")
}
The gateway never sees an implementation detail; it just reads directives and composes the graph. In 2024–2025, this pattern is spreading beyond Apollo as other vendors and open source projects adopt directive-driven composition.
These are some of the best examples of using directives in GraphQL because they show the real payoff: directives let infrastructure make smart decisions based on schema metadata.
Client-side examples: using directives in GraphQL queries
So far we’ve focused on schema directives, but query directives are just as important on the client side.
Conditional fields in a mobile app
Imagine a React Native app that only needs heavy data on Wi‑Fi. You can wire that into your variables:
query Feed($richMode: Boolean!) {
feed {
id
title
previewImage @include(if: $richMode)
videoUrl @include(if: $richMode)
}
}
Your network layer can set $richMode based on connection quality. This is a simple example of using directives in GraphQL that has a direct impact on performance and battery life.
@defer and @stream (where supported)
Some servers support experimental directives like @defer and @stream to send partial results:
query ProductPage {
product(id: "123") {
id
name
heroImage
reviews @defer {
rating
text
}
}
}
These examples include more advanced delivery strategies, where directives control when and how results are sent over the wire.
Design tips: when to introduce a custom directive
Because it’s easy to go overboard, it’s worth asking a few questions before adding a new directive:
- Is this logic cross-cutting across many fields or types? If yes, a directive is a good candidate.
- Can this be expressed more simply as a normal field or argument? If yes, skip the directive.
- Do you expect tooling (gateway, registry, linter, CI) to understand this metadata? Directives are perfect for that.
- Will this be readable to a new engineer six months from now? If the answer is “probably not,” rethink the design.
The best examples of using directives in GraphQL are the ones that make your schema more self-describing and your operational rules easier to audit.
For broader API design guidance and trade-offs, resources from universities and standards bodies are worth browsing, even if they’re not GraphQL-specific. For instance, Harvard’s CS courses and materials on systems design (https://cs.harvard.edu) often discuss similar concerns around abstraction and policy.
FAQ: common questions about directive usage
What are some practical examples of directives in a production GraphQL API?
Real examples include @auth for access control, @rateLimit for throttling, @cacheControl or @edgeCache for caching hints, @sensitive and @mask for privacy, @feature for feature flags, and federation directives like @key and @requires for composed graphs.
Can I create my own example of a directive that works across multiple services?
Yes. In a federated or gateway-based setup, you can define a shared directive (for example, @version or @sensitive) and teach the gateway or schema registry to interpret it. That way, every subgraph can use the same directive and you still get centralized behavior.
Do directives hurt performance?
Not by themselves. A directive is just metadata. Performance depends on what you do with it: calling an external rate limit service, hitting a feature flag API, or performing extra auth checks. Many of the best examples of using directives in GraphQL actually improve performance by enabling selective fetching and better caching.
Are there examples of using directives in GraphQL on the client only?
Yes. The built-in @include and @skip directives, plus experimental ones like @defer and @stream, live purely in queries. Clients use them to control which fields to fetch and when to receive them, without any schema changes.
When should I avoid adding a directive?
If the behavior is purely business logic for a single field and doesn’t need to be visible to tooling or infrastructure, a normal resolver is usually cleaner. Reserve directives for cross-cutting concerns, schema documentation, or behaviors that gateways, registries, or clients need to understand.
Related Topics
Best examples of using fragments in GraphQL API (with real patterns)
Practical examples of using directives in GraphQL (with real patterns)
Real-world examples of GraphQL with Node.js: 3 practical builds you can copy
Real-world examples of optimizing GraphQL queries
Real-world examples of GraphQL with TypeScript: practical examples for 2025
Examples of Schema Definition in GraphQL: 3 Practical Examples for Real APIs
Explore More GraphQL API Examples
Discover more examples and insights in this category.
View All GraphQL API Examples