Exploring GraphQL Directives in the Wild


Due to their unrestricted nature, directives are one of GraphQL's more powerful features, enabling GraphQL servers to provide custom capabilities not supported by the GraphQL spec, and offering a glimpse of what GraphQL may look like in the future.
Directives are suitable for those generic functionalities that can be applied across several parts of the application, such as:
- Analytics
- Caching
- Data formatting
- Data mocking
- Improving performance
- Input validation
- Logging
- Security
- Setting default values
- User authorization control
Some time ago, I wrote article GraphQL directives are underrated, expressing how we can benefit from directives. In this write-up I expand on the topic, attempting to find out what actual directives are out there, available to our use.
I will do a discovery tour of directives in the wild, as offered by different GraphQL servers, tools and services, and including both schema-type directives (used to help build the GraphQL schema) and query-type directives (used to alter the response when resolving the query).
Let's start!
Built-in directives
The following directives are part of the GraphQL spec, and must be supported by all GraphQL servers:
@specifiedBy
has been added to the spec very recently, so not all GraphQL servers support it yet. It is used to provide a specification URL for declaring how custom scalar types behave.
For instance, a custom scalar type UUID
is defined with a URL pointing to the relevant IETF specification (source):
scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122")
Building the schema
Many GraphQL servers and services make use of directives as a tool to build the schema.
StepZen, for instance, offers several such directives, including:
@rest
, to fetch the data for a GraphQL field from a REST endpoint.@dbquery
, to resolve a field by executing a query to the DB.@materializer
, to define connections among types.
In the query below, @rest
is used by field myArticles
, to fetch the logged-in user's articles posted to dev.to through its REST API:
type Article {
id: ID!
title: String!
description: String
}
type Query {
myArticles(username: String!): [Article]
@rest(endpoint: "https://dev.to/api/articles?username=$username")
}
Directives can be used not only to add new fields to the GraphQL schema, but also to augment existing fields with extra functionality.
For instance, by adding the @search
directive to field Post.datePublished
in Dgraph (source):
type Post {
...
datePublished: DateTime @search
}
...the field queryPost
will then accept the published date in the filter
argument, which filters those nodes that satisfy certain condition:
{
queryPost(
filter: {
datePublished: {
ge: "2020-06-15"
}
}
) {
...
}
}
Improving performance
Directives @defer
and @stream
currently have their implementation being worked out in graphql-js
and will, eventually, become part of the GraphQL specification.
These directives make GraphQL support incremental delivery of data. They enable the clients to tell the GraphQL server what is the relative priority of the requested data, as to immediately obtain higher-priority data with as low latency as possible, and lower-priority data on a subsequent response, thus improving the apparent performance of the application.
For instance, in the query below, by setting @stream
's argument initialCount
to 1
, we indicate that the list from friends
must initially bring only 1 element, with all the other elements to be brought in a subsequent response, and the fragment GroupAdminFragment
will similarly not be resolved on the initial request (source):
{
viewer {
id
friends(first: 5) @stream(initialCount: 1, label: "friendStream") {
id
}
...GroupAdminFragment @defer(label: "groupAdminDefer")
}
fragment
GroupAdminFragment {
managed_groups {
id
}
}
}
Another way to improve performance is, paradoxically, to do the opposite approach: to combine several queries into one.
This can help when we need to execute a mutation and, referencing data from a newly created entity, execute yet another mutation. This procedure would normally involve two separate requests, but we can execute both of them together in a single request, via the @export
directive.
For instance, the GraphQL API for WordPress can search for blog posts containing the name of the logged-in user, by executing two queries in the same request (source):
query GetUserName {
me {
name @export(as: "_authorName")
}
}
query GetPostsContainingUserName($_authorName: String = "") {
posts(searchfor: $_authorName) {
id
title
}
}
Creating unique IDs
When the DB doesn't have a single incremental ID for all resource types, two objects from two different types may share the same ID. Directives can help create a global, unique ID for each object.
graphql-tools
describes how to synthesize unique IDs via a @uniqueID
directive, which combines the object type with its ID, making it globally unique (source):
type Query {
people: [Person]
}
type Person @uniqueID(name: "uid", from: ["personID"]) {
personID: Int
name: String
}
Likewise, the @computed
directive is a community-built directive that allows to define a new field, which concatenates the values from other fields on the same type. By appending the type to the id
field, we can create a globally unique ID (source):
type User {
id: String
uniqueID: String @computed(value: "User-$id")
}
Stitching and federation
Stitching and federation are two approaches to access the many GraphQL API services a company may have, via a single graph.
StepZen uses the @sdl
directive to assemble many separate schemas, each of them living on its own .graphql
file, into a unified GraphQL schema (source):
schema
@sdl(
files: [
"algolia/algolia.graphql"
"contentful/blog.graphql"
"contentful/person.graphql"
]
) {
query: Query
}
Apollo server supports schema federation, which is based on several directives, namely @external
, @requires
, @provides
, @key
and @extends
. Through these directives, the local GraphQL schema can be mapped to an external schema (source):
type Review @key(fields: "id") {
id: ID!
body: String
author: User
product: Product
}
extend type User @key(fields: "email") {
email: String! @external
}
extend type Product @key(fields: "upc") {
upc: String! @external
}
Modifying the shape of the JSON response
We know that in GraphQL, the shape of the response will match one to one the shape of the query. However, sometimes we may want to tweak the shape of the response.
One such instance is the requested flat chain syntax feature, which flattens multiple levels of the response into a single one, so these results are easier to retrieve.
This use case can be satisfied for Node.js servers using the @_
directive provided by GraphQL Lodash. By using this directive, instead of spanning the multiple levels from the query, the results can be arranged as a map of key
=> value
, such as person => list of films (source):
{
peopleToFilms: allPeople @_(get: "people") {
people @_(keyBy: "name", mapValues: "filmConnection.films") {
name
filmConnection {
films @_(map: "title") {
title
}
}
}
}
}
...producing this response:
{
"data": {
"peopleToFilms": {
"Luke Skywalker": [
"A New Hope",
"The Empire Strikes Back",
"Return of the Jedi",
"Revenge of the Sith"
],
"C-3PO": [
"A New Hope",
"The Empire Strikes Back",
"Return of the Jedi",
"The Phantom Menace",
"Attack of the Clones",
"Revenge of the Sith"
]
}
}
}
Dgraph can also remove nesting in the response, via the @normalize
directive (source):
{
director(func:allofterms(name@en, "steven spielberg")) @normalize {
director: name@en
director.film {
film: name@en
initial_release_date
starring(first: 2) {
performance.actor {
actor: name@en
}
performance.character {
character: name@en
}
}
country {
country: name@en
}
}
}
}
...which produces:
{
"data": {
"director": [
{
"director": "Steven Spielberg",
"film": "Hook",
"actor": "John Michael",
"character": "Doctor",
"country": "United States of America"
},
{
"director": "Steven Spielberg",
"film": "Hook",
"actor": "Brad Parker",
"character": "Jim",
"country": "United States of America"
}
]
}
}
Combining results
GraphQL Lodash's @_
directive can not only modify the shape of the results, but also combine/filter values according to some operation, so we do not need to execute that operation on the client.
For instance, in the query below, instead of obtaining a list of planets with their population, and then calculating the one with the biggest population on the client, the maxBy
argument indicates that we want to execute that operation on the server when resolving the query (source):
{
planetWithMaxPopulation: allPlanets @_(get: "planets") {
planets @_(maxBy: "population") {
name
population
}
}
}
...this query produces:
{
"data": {
"planetWithMaxPopulation": {
"name": "Coruscant",
"population": 100000000000000
}
}
}
Omitting null
items from the response
In certain situations, we may want to omit the response of the field when it is null
. This feature has been rejected for the spec, so a directive can fill the void.
In the query below, directive @removeIfNull
provided by the GraphQL API for WordPress will not output the featuredImage
field whenever it is null
(source):
{
posts {
id
title
hasFeaturedImage
featuredImage @removeIfNull {
src
}
}
}
Mocking data
Through directive @mock
, StepZen provides a way to retrieve mock data for any field, which is useful to speed-up building the schema (source):
interface Character @mock {
id: ID!
name: String!
isMonster: Boolean!
episodeID: ID!
}
type Query {
character(id: ID!): Character
characters(isMonster: Boolean!): [Character]
}
When querying, we will obtain "lorem ipsum" data:
{
"data": {
"character": {
"name": "In vel mi sit amet augue congue elementum"
}
}
}
Formatting fields
Directives shine in the realm of formatting fields, because the same functionality can be applied across fields, independently of which field it is.
For instance, any String
field can be converted to upper case, hence it makes much more sense to provide a single @upperCase
directive, than to add a corresponding field argument convertToUpperCase
all over.
GraphQL servers may ship with some directives to format fields, and also enable developers to implement their own custom directives.
That is the case with GraphQL Java, which documents how to implement a @dateFormat
directive (source):
directive @dateFormat on FIELD_DEFINITION
type Query {
dateField: String @dateFormat
}
A few libraries provide many ready-to-use directives to GraphQL servers based on Node.js.
graphql-custom-directives
provides many directives to format strings:
@date
@number
@currency
@lowerCase
@upperCase
@camelCase
@startCase
@capitalize
@kebabCase
@trim
@default
@toLower
@toUpper
@template
@phone
graphql-directives
provides similar functionality and, in addition, it enables to convert measurements among different units, in its set of directives for the Apollo server:
- Currencies:
@formatCurrency
- Dates:
@formatdate
- Numbers:
@formatNumber
- Phone Numbers:
@formatPhoneNumber
- Strings:
@camelCase
@capitalize
@deburr
@kebabCase
@lowerCase
@lowerFirst
@snakeCase
@toLower
@toUpper
@trim
@upperCase
@upperFirst
- Measurements:
@converLength
@convertSurfaceArea
@convertVolume
@convertLiquidVolume
@convertAngle
@convertTime
@convertMass
@convertTemperature
@convertForce
@convertEnergy
@convertPower
@convertPressure
@convertBinary
Validating user access
Another use case where directives shine is in validating user access, as to make sure that only those users with the right permissions can access certain field, for any field in the schema.
graphql-tools
provides an opinionated way to implement the GraphQL schema for Node.js servers. The docs demonstrate how to implement an @auth
directive to validate user authentication (source):
directive @auth(requires: Role = ADMIN) on OBJECT | FIELD_DEFINITION
enum Role {
ADMIN
REVIEWER
USER
UNKNOWN
}
type User @auth(requires: USER) {
name: String
banned: Boolean @auth(requires: ADMIN)
canPost: Boolean @auth(requires: REVIEWER)
}
Authentication based on a different set of directives is also available. For instance, this tutorial explains how to set-up user authentication for Node.js servers, based on JWT tokens and the following custom directives:
@isAuthenticated
, to determine if the user has been authenticated@hasScope
, to determine if the user has access to a pre-defined scope
Some libraries already provide this authentication mechanism, ready-to-use. This is the case with graphql-auth-directives
and graphql-directive-auth
, which in addition provide directive @hasRole
, allowing to validate if the user is the admin.
Concerning a different use case, Dgraph uses directive @secret
when building the schema, to define a field as holding a password. Such a field is treated differently than all others, encrypting it before saving to the DB. The value for these fields cannot be queried; instead, we can only check if a provided password matches the one stored in the DB (source):
type Author @secret(field: "pwd") {
name: String! @id
}
query {
checkAuthorPassword(name: "myname", pwd: "mypassword") {
name
}
}
Rate limiting
Related to validating user access, is restricting how many times can a visitor execute queries in a certain amount of time, also known as rate limiting.
This mechanism helps repel brute force attacks by malicious actors trying to gain access into the GraphQL service, by executing a huge number of queries (such as log-in attempts with different passwords, or 2FA tokens, or others), expecting to find the one value that unlocks access to the service.
Portara is a rate limiter for Apollo Server. Its directive @portara
allows to specify how many requests can be executed by a visitor (as identified by the IP) within a certain amount of time, or if to throttle the request (source):
type Query @portara(limit: 10, per: "5 seconds", throttle: 0) {
hello: String! @portara(limit: 15, per: "5 seconds", throttle: 0)
goodbye: String!
}
Similarly, the Tartiflette docs describe how to implement a @rateLimiting
directive (source):
directive @rateLimiting(
name: String!
maxAttempts: Int! = 5
duration: Int! = 60
) on FIELD_DEFINITION
input RecipeInput {
id: Int!
name: String
cookingTime: Int
}
type Mutation {
updateRecipe(input: RecipeInput!): Recipe!
@rateLimiting(name: "update_recipe")
}
Validating constraints
Another excellent use for directives is to validate contraint on field inputs.
The docs for GraphQL.NET describe how to code a @length
directive, to validate that the input is between min
and max
chars long (source):
directive @length(
min: Int
max: Int
) on INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION
Library graphql-constraint-directive
for Node.js servers provides the @constraint
directive, allowing to validate a multitude of conditions for String
and Int
inputs (source):
@constraint(minLength: 5)
What constraints to apply must be specified via directive arguments, including:
- For
String
s:minLength
maxLength
startsWith
endsWith
contains
notContains
pattern
format
(supportingbyte
,date-time
,date
,email
,ipv4
,ipv6
,uri
, anduuid
)
- For
Int
s:min
max
exclusiveMin
exclusiveMax
multipleOf
The constraint could also be validated against the database. Prisma v1, for instance, uses the @unique
directive to make sure that no two records of a same model can have the same value for a certain field, such as the user's email
(source):
type User {
id: ID! @id
email: String! @unique
name: String!
}
Caching
Caching helps improve the application's performance, and it can be applied on several layers.
When operating GraphQL via persisted queries, the response can be cached via standard HTTP Caching, defining the max-age
on a field-by-field basis using a @cacheControl
directive (source):
directive @cacheControl(maxAge: Int) on FIELD_DEFINITION
type User {
id: ID @cacheControl(maxAge: 31557600)
url: URL @cacheControl(maxAge: 86400)
name: String @cacheControl(maxAge: 3600)
karma: Int @cacheControl(maxAge: 60)
}
type Root {
me: User @cacheControl(maxAge: 0)
}
Whenever executing expensive operations to calculate the value of some field, GraphQL by PoP enables to cache those results to hard disk or memory, for an established amount of time, via a @cache
directive (source):
{
posts {
id
title @translate(from: "en", to: "es") @cache(time: 10)
}
}
This server, additionaly, allows to manipulate the src
field of media elements, replacing their origin domain with a corresponding CDN domain, via a @cdn
directive (source):
{
posts {
id
featuredImage {
src
@cdn(
from: "https://newapi.getpop.org"
to: "https://nextapi.getpop.org"
)
}
}
}
Handling errors
In GraphQL, an empty result for some field is normally treated as a recoverable error, returning status code 200
with the description of the problem. However, some empty field could also be considered a non-recoverable error, possibly returning status code 500
instead.
As demonstrated in this blog post concerning graphql-js
, a @principalField
directive can be used to remove the ambiguity of which fields will produce recoverable or non-recoverable errors:
{
artwork(id: "andy-warhol-skull") {
mainContentStuff @principalField
biographicalData
userReviews {
...
} # Accesses a back-end reviews service
...
}
}
Tracing
When building the GraphQL service, directives can be used to print additional output, to help us troubleshoot the application, or measure its performance.
For instance, the GraphQL API for WordPress offers a @traceExecutionTime
directive, which prints how much time it takes to resolve those fields the directives is applied to (source):
{
posts {
id
title @traceExecutionTime
}
}
Interacting with the API service
Directives can also be used to manipulate the underlying API service, applying some behavior.
AWS Amplify, for instance, has the 3rd-party directive @ttl
that enables DynamoDB's time-to-live feature to auto-delete old entries (source):
type ExpiringChatMessage @model {
id: ID!
message: String
expirationUnixTime: AWSTimestamp! @ttl
}
Lighthouse PHP provides several directives that enable it to interact with the GraphQL server whenever some condition occurs:
@broadcast
, to broadcast the results of a mutation to subscribed clients.@event
, to dispatch an event after the resolution of a field.
Interacting with external APIs
A directive can modify the value of a field, by applying some desired functionality on it. In this aspect, directives are suitable to interact with external, cloud-based services, as to allow them to modify the results from the GraphQL API.
The GraphQL API for WordPress allows you to translate the results of the query to multiple languages by having its @translate
directive interact with the Google Translate API (source):
{
posts {
id
title @translate(from: "en", to: "fr")
}
}
Customizing scores for query complexity analysis
Query complexity analysis is a method for restricting the execution of expensive queries (as to repel attacks by malicious actors), based on the calculation of a total score for the query, and validating that this score doesn't surpass the maximum allowed score.
The total score is computed based on the scores for each element present in the query. While setting a default score, Lighthouse PHP also provides a @complexity
directive, which allows you to use a custom resolver to calculate the score for some field (source):
type Query {
posts: [Post!]!
@complexity(resolver: "App\\Security\\ComplexityAnalyzer@userPosts")
}
Conclusion
Directives are a wonderful mechanism to augment our GraphQL APIs in a myriad of different ways, and leveraged by different GraphQL servers, services and tools.
This article shows a great number of use cases for directives. Even though we won't be able to use all demonstrated directives for our own application (for instance, we can't use the libraries providing directives to JavaScript servers, if we use a GraphQL server in some other language), we can still appreciate the many possibilities offered by directives, and possibly implement them for our own solutions.