What are GraphQL directives?
From the specification, “Directives provide a way to describe alternate runtime execution and type validation behavior in a GraphQL document.” Essentially, directives in the GraphQL specification are annotations that can be used to alter the behavior of a query or mutation. They’re easy to recognize because they are always prefixed with the “@” character. The GraphQL specification itself states that all spec-compliant schemas need to implement @skip
, @include
, @deprecated
, and @specifiedBy
– but more on those, and the upcoming standard directive implementations, later.
Where can GraphQL directives be used?
Directives can only be used in the places their specification allows for, namely, what’s defined after the “on” statement of a directive definition. For example, from the GraphQL schema specification, this is what a “deprecated” directive definition looks like.
directive @deprecated(
reason: String = "No longer supported"
) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE
Note that the locations defined after “on” are where this directive can be used. Beyond that, the specification is very open where directives can be used! As defined in the literature, directives can be created on either executable or type system locations. Type system directive locations are: SCHEMA
, SCALAR
, OBJECT
, FIELD_DEFINITION
, ARGUMENT_DEFINITION
, INTERFACE
, UNION
, ENUM
, ENUM_VALUE
, INPUT_OBJECT
, and INPUT_FIELD_DEFINITION
. Executable directive locations are QUERY
, MUTATION
, SUBSCRIPTION
, FIELD
, FRAGMENT_DEFINITION
, FRAGMENT_SPREAD
, and INLINE_FRAGMENT
. That means it’s open for implementation as each API provider seems fit.
Directives can live on fields, inputs, operations, and more.
Example of an operation directive:
query GetItems @cached {
id
}
Example of a field directive:
query GetItems($shouldNotGetNames: Boolean) {
id
names @skip(if:$shouldNotGetNames)
}
What do GraphQL directives do?
Returning again to our opening statement that directives allow us to modify the behavior of our queries and mutations, let’s look at the default behavior of the standardized directives, the behavior of the Hasura specific directives, and some generally creative implementations we’ve seen in the larger GraphQL community.
Current Specification Directives
@deprecated
The @deprecated
directive defines fields that are no longer supported or will likely be replaced soon. Given the iterative nature of a GraphQl endpoint, being able to let consumers know that a specific field they are using will be replaced, along with a reason and direction on what to use instead is an essential method for communicating changes to the API design.
@skip
The @skip
directive lets you specify when the following field should not be returned based on a truthy condition.
@include
The @include
directive lets you specify when the following field should be returned based on a truthy condition. This is the opposite of the @skip directive but likewise allows for choosing when a field should or should not be returned.
@specifiedBy
@specifiedBy
provides type references as to where the specific field type is defined in the literature. If you want to explain which date type or what the specifics around the use of a UUID
type are, you can link to their official reference documentation.
Future Specification Directives
Github RFC for reference: https://github.com/graphql/graphql-js/issues/2848
@defer with args
When fetching data from data layers where not all content priority should be treated equally, the @defer
directive lets you explain when it’s ok to resolve data early. As the HTTP/2 spec becomes more widely implemented, we’ll see an uptick in best practices recommending the use of @defer
. This argument takes a label input that labels the response for front-end orchestration
@stream with args
@stream
lets the user define how many items the API should respond with before the initial response. Where there may be a few hundred results from the server, the @stream
directive lets you define a minimum required number before getting the first batch of results sent to the server. This is also technology-dependent and as we see specifications like HTTP/2 making headway in adoption, we’ll see these directives gain more precedence. This input takes an initialCount argument for defining what the threshold is along with a label argument for identifying the follow-up payload from this server.
Hasura defined Custom Directives
@cached
The Hasura API enables an additional directive called @cached
that enables automatic caching on Hasura Cloud. The arguments for this directive are ttl (time to live), defined in seconds with an upper bound of 300 seconds, and a boolean refresh argument to declare if the cache should be automatically cleared or not. You can read more about the @cached
directive and test it out with an interactive example here.
@rest
Perhaps a more common use-case among API designers would be implementing a directive like this @rest
directive. Its arguments take a REST endpoint and allow for resolving the data that lives at the specified endpoint. This would be quite common for data-federation architectures.
@dbquery
Similar to the data resolver pattern seen in the @rest
design, @dbquery
resolves data at the specified database endpoint with the specified query syntax. It’s a query within a query. While the overhead may be problematic for long-term maintenance, this brute-force approach helps prototype GraphQL endpoints quickly.
@materializer
This query directs the requested fields to be executed against a secondary GraphQL endpoint, combining the end result into a single API.
@external
A similar pattern as @materializer
, @external
indicates that the requested data is owned by an “external” service.
@requires(fields: _FieldSet!)
For services requiring annotated APIs for federation, the @requires
directive lets a service identify the needed data to resolve fields from an external dataset.
@provides(fields: _FieldSet!)
Let a consuming service know which fields are guaranteed to be in the response and thus, allows the consuming service to make predictable guarantees about that data shape.
@key(fields: _FieldSet!)
When federating data sources, it’s important to track uniqueness across datasets, and this directive allows for declaring which fields or which product of fields will guarantee that uniqueness.
@_
Perhaps one of the most interesting examples of directive usage, this concept creates a breakout utility set for accessing lodash style utility helpers on queries and query responses. If formatting data at the request stage is something you need to accommodate for, this might just be the tool you have been looking for.