Practical examples of using directives in GraphQL (with real patterns)

If you’re building or maintaining a GraphQL API, sooner or later you’ll go hunting for practical examples of using directives in GraphQL that go beyond the textbook @include and @skip. Directives are the switchboard of GraphQL: they let you alter query behavior, apply conditional logic, and plug in cross‑cutting concerns without rewriting your schema every week. In this guide, we’ll walk through real examples of examples of using directives in GraphQL that show up in production APIs: feature flags, auth checks, rate limiting, field masking for GDPR, caching hints, and more. Instead of abstract theory, we’ll look at how these directives fit into everyday workflows in 2024–2025 GraphQL stacks, from Node and TypeScript servers to federated schemas and edge runtimes. Along the way, you’ll see how the best examples of directives help teams keep schemas stable while product requirements keep changing. If you’ve ever thought “there has to be a cleaner way to do this in GraphQL,” this is where directives usually enter the picture.
Written by
Jamie
Published

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.

Explore More GraphQL API Examples

Discover more examples and insights in this category.

View All GraphQL API Examples