What Are GraphQL Directives?
GraphQL directives serve as annotations within a GraphQL schema or query operation, indicating that the annotated element requires special evaluation or processing. They enable modifications in runtime execution and type validation within a GraphQL document, providing a standardized way to extend GraphQL's functionality beyond what field arguments alone can accomplish. Directives appear in GraphQL documents with the @ symbol followed by the directive name and optional arguments in parentheses, allowing developers to conditionally include fields, mark deprecated elements, or document custom scalar types.
The GraphQL specification defines four built-in directives that every developer should understand: @skip and @include for conditional field execution during queries, @deprecated for API evolution and schema migration, and @specifiedBy for documenting custom scalar specifications. These directives form the foundation upon which many custom directive patterns are built, making them essential knowledge for anyone working with GraphQL APIs professionally.
Built-in directives differ from custom directives in that they are guaranteed to be available in any GraphQL implementation that adheres to the specification. Custom directives, created by server frameworks or tools, add specialized functionality like authentication or field-level authorization. Understanding the built-in directives first provides a solid conceptual framework for working with more advanced directive implementations you'll encounter in enterprise GraphQL deployments.
Each directive serves a specific purpose in GraphQL development
@skip
Conditionally excludes fields from query execution when the if argument is true. Useful for optional field fetching and performance optimization.
@include
Conditionally includes fields when the if argument is true. The logical opposite of @skip for readable conditional logic.
@deprecated
Marks fields or enum values as deprecated in the schema. Provides migration guidance through the reason argument.
@specifiedBy
Documents custom scalar types with a URL to their specification. Helps clients understand scalar semantics.
The @skip Directive
The @skip directive conditionally excludes a field or fragment from query execution based on a boolean argument. When the if argument evaluates to true, the annotated element is skipped entirely--its resolver is not called, and no data is returned. This behavior has significant performance implications: when a field is skipped, the GraphQL executor never invokes its resolver, avoiding unnecessary computation or database queries.
The formal syntax for @skip follows the GraphQL specification:
directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
The if argument is required and must be a Boolean value. It can be a literal value or, more commonly, a variable reference passed through query variables. Using variables allows clients to dynamically control field inclusion without modifying the query structure itself, enabling flexible data fetching strategies that adapt to different UI states or user permissions.
When implementing optional field fetching, @skip proves particularly valuable for expensive operations like activity logs, analytics data, or nested relationships that may not always be needed. By allowing clients to indicate their interest level at runtime, you can optimize server-side resource utilization while maintaining a single, flexible query interface for your web development projects.
1query GetUser($includeActivity: Boolean!) {2 user(id: "123") {3 id4 name5 email6 activityLog @skip(if: $includeActivity)7 }8}9 10# When $includeActivity is true, activityLog is not included in the response11# When $includeActivity is false, activityLog is included as normalThe @include Directive
The @include directive conditionally includes a field or fragment in query execution, serving as the logical inverse of @skip. When the if argument evaluates to true, the annotated element is processed normally; when false, the field or fragment is omitted from both execution and response. This predictable behavior makes @include an excellent choice for implementing optional field expansion in your GraphQL API.
The directive syntax mirrors @skip:
directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
Like @skip, the if argument is required and accepts either literal values or variables. A common pattern involves using @include for optional nested data--reviews, shipping information, or related entities that clients may or may not need depending on the current view. A single query can thus serve multiple client requirements, showing basic information, expanded details, or complete data structures depending on which optional sections are included.
The directive also works effectively with inline fragments, enabling conditional type-specific field selection. This proves especially valuable for union types or interfaces where different client views might need different type extensions included or excluded based on feature flags or user permissions.
1query ProductDetails($expandReviews: Boolean!, $expandShipping: Boolean!) {2 product(id: "456") {3 id4 name5 price6 reviews @include(if: $expandReviews) {7 id8 rating9 comment10 }11 shippingInfo @include(if: $expandShipping) {12 estimatedDays13 carrier14 }15 }16}@skip Vs @include: Choosing The Right Directive
While @skip and @include are functionally equivalent--just with opposite boolean logic--the choice between them matters for code readability and maintenance. @include reads more naturally when expressing "include this when X is true," making it intuitive for optional data that should appear conditionally. @skip reads better when expressing "skip this when X is true," which some teams prefer for permission checks or debug information.
There's also a subtle but important distinction in default behavior. With @include, omitting the variable results in the field being included by default. With @skip, omitting the variable results in the field being skipped. This difference can matter in scenarios where you want specific default behaviors for optional variables.
Best practice: Choose one convention and apply it consistently across your codebase. Document which approach your team follows and resist the temptation to mix styles. Consistency makes queries easier to read and reduces cognitive overhead when reviewing code changes.
| Consideration | Use @include When | Use @skip When |
|---|---|---|
| Readability | "Include when true" reads naturally | "Skip when true" reads naturally |
| Default behavior | You want fields included by default | You want fields skipped by default |
| Common pattern | Optional data expansion | Permission checks, debug fields |
The @deprecated Directive
The @deprecated directive marks a field or enum value as deprecated, signaling that it should not be used in new client applications and may be removed in future versions. Unlike @skip and @include, which are executable directives applied in queries, @deprecated is a type system directive--it only appears in schema definitions, not in query operations. This distinction means @deprecated affects schema introspection and client tooling, but does not alter query execution behavior.
The formal syntax includes an optional reason argument with a default value:
directive @deprecated(
reason: String = "No longer supported"
) on FIELD_DEFINITION | ENUM_VALUE
The reason argument provides crucial migration guidance, telling clients exactly what to use instead. This reason appears in introspection responses, allowing tools like GraphiQL or Apollo Studio to display deprecation warnings. Effective deprecation reasons are specific and actionable--vague messages like "No longer supported" provide little value to developers trying to migrate their code.
Importantly, marking a field as @deprecated does not prevent queries from using it. Clients can still request deprecated fields and receive data normally. The directive is purely informational, communicating that the field may be removed in future schema versions. This allows for graceful migration periods where both old and new field implementations coexist, an essential practice for maintaining stable web APIs over time.
1type User {2 id: ID!3 firstName: String!4 lastName: String!5 # Use fullName instead6 fullName: String @deprecated(reason: "Use firstName and lastName fields instead")7 email: String!8 createdAt: String!9}1{2 __type(name: "User") {3 fields {4 name5 isDeprecated6 deprecationReason7 }8 }9}Deprecating Enum Values
Enum values can also be deprecated using the same @deprecated directive, applied to individual ENUM_VALUE locations. This capability proves essential when status values or categorical options need to be phased out over time. Rather than breaking existing clients by removing an enum value entirely, deprecation allows for a transition period where clients can migrate to newer alternatives while maintaining compatibility.
A common pattern involves deprecated status values that have been replaced with more specific alternatives. For example, a legacy "CANCELLED" status might be replaced with "orderCancelled" and "subscriptionCancelled" to provide clearer semantics. By deprecating the generic value and providing specific alternatives, you enable gradual migration without service disruption.
For enums that should never receive new values--such as fixed status codes that mobile clients must handle comprehensively--consider combining @deprecated with other schema patterns. Some implementations use @final to indicate that an enum's value set is stable and should not be extended, complementing @deprecated for elements being phased out.
1enum OrderStatus {2 PENDING3 PROCESSING4 SHIPPED5 DELIVERED6 # Deprecated values7 CANCELLED @deprecated(reason: "Use orderCancelled instead")8 REFUNDED @deprecated(reason: "Use orderRefunded instead")9}The @specifiedBy Directive
The @specifiedBy directive documents custom scalar types by providing a URL that points to the scalar's formal specification. Custom scalars like DateTime, Email, URL, or ISBN extend GraphQL's type system with domain-specific validation, but without documentation, clients cannot easily understand how to properly handle or validate values of these types. @specifiedBy bridges this gap by linking custom scalars to their authoritative specifications.
The directive syntax is straightforward:
directive @specifiedBy(url: String!) on SCALAR
The url argument must point to a specification that defines the format, validation rules, and serialization behavior of the scalar. Using established standards like RFC documents for email addresses or date-time formats ensures interoperability with client libraries that understand those specifications. This approach proves especially valuable in multi-client ecosystems where different clients need consistent validation behavior.
When defining custom scalars, always consider what specification can accurately describe their behavior. For DateTime, the RFC 3339 profile of ISO 8601 provides a well-understood standard. Email validation can reference RFC 5322, while URLs should link to the WHATWG URL Standard. These specifications provide client libraries with the information needed to implement proper validation and serialization, supporting robust API development.
1scalar DateTime @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc3339")2scalar Email @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc5322")3scalar URL @specifiedBy(url: "https://url.spec.whatwg.org/")4scalar ISBN @specifiedBy(url: "https://isbn-international.org/iso2108")Schema Directives Vs Query Directives
Understanding the distinction between type system directives and executable directives is fundamental to using GraphQL effectively. Type system directives like @deprecated and @specifiedBy annotate schema definitions and are processed when the server builds or validates the schema. They provide metadata about the schema itself--marking deprecated elements, documenting custom scalars, or indicating authentication requirements. These directives never appear in query operations; they exist purely in the schema definition language.
Executable directives like @skip and @include, by contrast, appear in query operations and are processed during query execution at runtime. When a query arrives with these directives, the GraphQL executor evaluates them as it processes each field, conditionally including or excluding elements based on variable values. This dynamic behavior enables flexible data fetching strategies that adapt to client needs.
The GraphQL specification defines specific locations where each type of directive can be applied. Type system directive locations include FIELD_DEFINITION, SCALAR, OBJECT, ENUM_VALUE, and others that describe schema structure. Executable directive locations include FIELD, FRAGMENT_SPREAD, INLINE_FRAGMENT, and QUERY/MUTATION/SUBSCRIPTION that describe query operations. This separation ensures directives are only used in appropriate contexts.
| Aspect | Type System Directives | Executable Directives |
|---|---|---|
| Examples | @deprecated, @specifiedBy | @skip, @include |
| Applied In | Schema definitions | Query operations |
| Processing Time | Schema build/validation | Query execution |
| Directive Locations | FIELD_DEFINITION, SCALAR, ENUM_VALUE | FIELD, FRAGMENT_SPREAD |
| Affects | Schema metadata | Query execution behavior |
Best Practices For Using Directives
When designing GraphQL schemas and queries, prefer field arguments over directives whenever possible. Field arguments integrate better with GraphQL's type system, cache more effectively in client-side caching libraries like Apollo Client, and are better understood by development tools and IDEs. Directives should be reserved for functionality that arguments cannot provide--conditional execution, schema metadata, and specialized processing that varies per query.
Caching considerations become important when using directives at scale. Normalized caching in Apollo Client and similar tools may not understand custom directive behavior, potentially leading to incorrect cache invalidation. While built-in directives like @skip and @include are generally well-handled by caching systems, custom directives require careful testing and potentially special handling for normalized caching to work correctly.
Effective deprecation requires clear migration paths and reasonable timelines. The reason argument should tell clients exactly what to use instead, not just that something is deprecated. Monitor usage of deprecated fields to coordinate migration efforts with client teams, and maintain deprecated fields for a reasonable period--typically several months at minimum--to give clients time to update their code.
Documentation matters for all directives, especially custom ones. Document available directives including their locations, arguments, and effects. Provide examples showing common use cases and migration guidance for deprecated elements. Client tools like GraphiQL can display directive documentation through introspection when properly configured.
Frequently Asked Questions
Conclusion
Built-in GraphQL directives provide essential functionality that every API developer should understand. The four specification-defined directives--@skip, @include, @deprecated, and @specifiedBy--enable conditional field execution, API evolution through graceful deprecation, and clear documentation of custom scalar specifications. These capabilities form the foundation for building maintainable, evolvable GraphQL APIs that can adapt to changing requirements without breaking existing clients.
The key to effective directive usage lies in understanding when to apply them versus traditional field arguments. Directives excel at dynamic behavior that varies per query, but field arguments typically provide better caching, tooling support, and type system integration. Reserve directives for cases where they provide functionality that arguments cannot achieve--conditional execution, schema metadata, and processing that must vary at runtime.
As GraphQL continues to evolve, new directives like @defer and @stream for incremental result delivery may become standardized. The patterns and concepts covered here provide a foundation for understanding and adopting these future capabilities. By mastering the built-in directives now, you position yourself to leverage advanced GraphQL features as they emerge and build APIs that serve clients reliably over time.
For teams building production GraphQL APIs, understanding directives connects directly to broader API design considerations including versioning strategies, performance optimization, and schema governance. These foundational concepts support the development of robust, maintainable web applications that scale with your organization's needs.
Need Help Implementing GraphQL In Your Project?
Our team of GraphQL experts can help you design and implement efficient APIs using best practices for directives, schema design, and performance optimization. From initial architecture to ongoing governance, we ensure your GraphQL implementation supports your business goals.
Sources
- GraphQL.js: Using Directives - Official documentation covering built-in directives and implementation details
- Zalando Engineering: Understanding GraphQL Directives - Enterprise-scale directive patterns from Zalando's production GraphQL implementation
- Tailcall: Unlocking the Power of GraphQL Directives - Comprehensive overview of directive categorization and practical examples
- GraphQL Specification: Directives - Official GraphQL specification defining directive syntax and behavior