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 Strings:
    • minLength
    • maxLength
    • startsWith
    • endsWith
    • contains
    • notContains
    • pattern
    • format (supporting byte, date-time, date, email, ipv4, ipv6, uri, and uuid)
  • For Ints:
    • 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.