How to use GraphQL Directives efficiently?
Tags: graphql, api, programming, javascript
Published: 2022-06-04 04:33:40

---
title: How to use GraphQL Directives efficiently?
published: true
date: 2022-06-03 22:09:47 UTC
tags: #graphql #api #programming #javascript
canonical_url: https://mayallo.com/how-to-use-graphql-directives-efficiently/
cover_image: https://cdn.hashnode.com/res/hashnode/image/upload/v1654293264062/A5MAse0W_.jpg?w=1600&h=840&fit=crop&crop=entropy&auto=compress
---

## Introduction

GraphQL is one of the most fantastic tools presented in the software world in the last few years. In fact, that’s for many [reasons](https://www.prisma.io/blog/top-5-reasons-to-use-graphql-b60cfa683511): its strongly typed schema, avoiding overfetching or underfetching, a handy tool for both server and client-side, composing multi API ([Stitching](https://www.graphql-tools.com/docs/schema-stitching/stitch-combining-schemas)), and the great community.

Actually, Directives are among the most powerful features of GraphQL that enable you to enhance and extend your API.

## What are Directives?

The directive is a function that decorates a portion of GraphQL schema to extend its functionality. For example `@UpperCase()` in the following example:

```graphql
type User {
	name: String! @UpperCase
}
```

Simply, this `@UpperCase` directive as its name implies would uppercase the user name and then return it.

## Directives Types

There are two types of directives:

- **Schema Directives**.
- **Query Directives**.

Let’s know the differences between them from the built-in directives.

Till now, there are four approved built-in directives:

- **`@deprecated(reason: String)`** which marks a portion of the schema as deprecated for an optional reason (Schema Directive).
    
    ```graphql
    type User {
    	fullName: String
    	name: String @deprecated(reason: "Use `fullName` instead")
    }
    ```
    
- **`@specifiedBy(url: String!)`** which provides a scalar specification URL for specifying the behavior of custom scalar types (Schema Directive).
    
    ```graphql
    scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122")
    ```
    
- **`@skip(if: Boolean!)`** if it is true, the GraphQL server would ignore the field and wouldn’t resolve it (Query Directive).
    
    ```graphql
    query getUsers($shouldSkipName: Boolean!) {
      name @skip(if: $shouldSkipName)
    }
    ```
    
- **`@include(if: Boolean!)`** if it is false, the GraphQL server would ignore the field and wouldn’t resolve it (Query Directive).
    
    ```graphql
    query getUsers($shouldIncludeName: Boolean!) {
      name @include(if: $shouldIncludeName)
    }
    ```
    

From the previous examples, you may notice that:

- The **Schema Directives** are defined in the schema itself and run while building it, and they are used by the schema designer.
- The **Query Directives** are used in the query and run while resolving it, and they are used by the end-user.

## Query Directives vs Field Argument

From the previous examples, you may ask why should we use directives while we can perform the uppercasing logic in the resolver itself depending on a field argument? This question will lead us to clarify the pros and cons of each other.

Unfortunately, different GraphQL servers, clients, and tools deal with GraphQL directives differently and support them to a different extent which makes conflict among them.

For example, [Relay](https://relay.dev/) doesn’t set into account using the Query Directive when querying the same field from the cache.

Take a look at the following example. This query runs for the first time then cached:

```graphql
query getPost(id: 1) {
	title # Hello World
}
```

Run the same query for the second time after caching but with the `@UpperCase` directive:

```graphql
query getPost(id: 1) {
	title @UpperCase # Hello World
}
```

The second query should return ‘HELLO WORLD’ however, Relay returns the same response as the first query which is existed in the cache even though we use the `@UpperCase` directive which is completely ignored.

From the previous example, you note that depending on Query Directives is inconsistent due to the different handling from the GraphQL providers, as a result, [GraphQL Tools](https://www.graphql-tools.com/) [discourages using the Query Directives](https://www.graphql-tools.com/docs/schema-directives#what-about-query-directives):

> *In general, however, schema authors should consider using field arguments wherever possible instead of query directives.*
> 

On the other hand, using directives has some advantages:

- Your code will be cleaner by improving its reusability, readability, and modularity which respects the [DRY Principle](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself).
- Respecting the [Single Responsibility Principle](https://en.wikipedia.org/wiki/Single-responsibility_principle).
- If you want your clients to extend your GraphQL API with new functionalities without touching your code, you can depend on directives to fulfill this task.

So to summarize this point, to be on the safe side, as GraphQL Tools advises, you should use the field arguments instead of Query Directives. So the previous example should be:

```graphql
query getPost(id: 1) {
	title(format: UPPERCASE) # HELLO WORLD
}
```

Or you can use the Query Directives only if you know what are you doing!

## Directives Use Cases

Basically, there are many possibilities for using custom directives. Let’s create some of them as practical examples and apply them on the same query `post`.

> *You can find the complete code 👉 [here](https://github.com/Mohamed-Mayallo/graphql-directives).*
> 

First of all, we will define this schema:

```graphql
type Post {
  id: Int!
  uuid: ID!
  userId: Int!
  title: String!
  body: String!
  createdAt: String!
}

type Query {
  post: Post
}
```

1. Let’s implement the upper-case directive and let’s call it `@upper`:
    
    Firstly, we need to define the [directive location](https://spec.graphql.org/October2021/#sec-Type-System.Directives).
    
    ```graphql
    directive @upper on FIELD_DEFINITION # This means, this directive can be applied on any field defined on any type (like `title`)
    ```
    
    Secondly, we need to define the directive transformer function that is responsible to apply the directive logic on every field having this directive.
    
    ```javascript
    function upperDirectiveTransformer(schema, directiveName) {
      return mapSchema(schema, {
        [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
          // `OBJECT_FIELD` is the mapperkind while `FIELD_DEFINITION` is location name in schema
          // Check whether this field has the specified directive
          const upperDirective = getDirective(schema, fieldConfig, directiveName)?.[0];
          if (upperDirective) {
            // Get this field's original resolver
            // If the original resolver is not given, then a default resolve behavior is used
            const { resolve = defaultFieldResolver } = fieldConfig;
            // Replace the original resolver with a function that *first* calls
            // the original resolver, then converts its result to upper case
            fieldConfig.resolve = async function (source, args, context, info) {
              const result = await resolve(source, args, context, info); // Calling the original resolver
              if (typeof result === 'string') return result.toUpperCase(); // Uppercasing the result
              return result;
            };
            return fieldConfig;
          }
        }
      });
    }
    ```
    
    Thirdly, we need to transform the schema by applying the directive logic.
    
    ```javascript
    schema = upperDirectiveTransformer(schema, 'upper');
    ```
    
    Fourthly, we can apply it to the `title` field as follows:
    
    ```javascript
    title: String! @upper
    ```
    
    That’s it, now you can use the `@upper` directive at any string field.
    
2. Let’s implement a directive that loads this post from a third-party API, and let’s call it `@rest(url: String!)`
    
    Let’s move on with the same steps
    
    ```graphql
    directive @rest(url: String!) on FIELD_DEFINITION
    ```
    <span></span>
    ```javascript
    function restDirectiveTransformer(schema, directiveName) {
      return mapSchema(schema, {
        [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
          const restDirective = getDirective(schema, fieldConfig, directiveName)?.[0];
          if (restDirective) {
            const { url } = restDirective; // Get the directive param
            const { resolve = defaultFieldResolver } = fieldConfig;
            fieldConfig.resolve = async function (source, args, context, info) {
              let { data } = await axios.get(url); // Use axios to get the post from a third-party
    		  // Inject the post in `args` to be able to return it from the resolver (You can apply your logic as you want)
              return await resolve(source, { ...args, post: data }, context, info);
            };
            return fieldConfig;
          }
        }
      });
    }
    ```
    <span></span>
    ```javascript
    async post(_, args) {
      return args.post; // Injected into `args` by the `@rest` directive
    }
    ```
    <span></span>
    ```javascript
    schema = restDirectiveTransformer(schema, 'rest');
    ```
    Now we can apply it to the `post` query as follows:
    ```graphql
    type Query {
      post: Post @rest(url: "https://jsonplaceholder.typicode.com/posts/1")
    }
    ```
    
3. Let’s implement another directive to optionally format the date of the post `createdAt` field and call it `@date(format: String = "mm/dd/yyyy")` :
    
    ```graphql
    directive @date(format: String = "mm/dd/yyyy") on FIELD_DEFINITION # Set a default format if not provided
    ```
    <span></span>
    ```javascript
    function dateDirectiveTransformer(schema, directiveName) {
      return mapSchema(schema, {
        [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
          const dateDirective = getDirective(schema, fieldConfig, directiveName)?.[0];
          if (dateDirective) {
            const { resolve = defaultFieldResolver } = fieldConfig;
            const { format } = dateDirective; // Get the directive param
            fieldConfig.resolve = async function (source, args, context, info) {
              const result = await resolve(source, args, context, info);
    		  if (!result) return null;
              try {
                return dateFormat(result, format);
              } catch {
                throw new ApolloError('Invalid Format!');
              }
            };
            return fieldConfig;
          }
        }
      });
    }
    ```
    <span></span>
    ```javascript
    schema = dateDirectiveTransformer(schema, 'date');
    ```
    
    Now, we can apply this directive to the `createdAt` field as follows:
    
    ```graphql
    createdAt: String! @date(format: "dddd, mmmm d, yyyy")
    ```
    
4. Let’s implement a directive to authorize the access to this post, let’s call it `@auth(role: String!)` :
    
    ```graphql
    directive @auth(role: String!) on FIELD_DEFINITION
    ```
    <span></span>
    ```javascript
    function authDirectiveTransformer(schema, directiveName) {
      return mapSchema(schema, {
        [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
          const authDirective = getDirective(schema, fieldConfig, directiveName)?.[0];
          if (authDirective) {
            const { resolve = defaultFieldResolver } = fieldConfig;
            const { role } = authDirective; // Get the directive param
            fieldConfig.resolve = async function (source, args, context, info) {
    		  // Check the authorization before calling the resolver itself
              if (role !== 'ADMIN') throw new ApolloError('Unauthorized!');
              return await resolve(source, args, context, info);
            };
            return fieldConfig;
          }
        }
      });
    }
    ```
    <span></span>
    ```javascript
    schema = authDirectiveTransformer(schema, 'auth');
    ```
    
    That’s it, now the directive is ready to apply:
    
    ```graphql
    type Query {
      post: Post @auth(role: "ADMIN")
    }
    ```
    
    Try to check on `OPERATOR` instead to validate the directive effect.
    
5. If we want to auto-generate a UUID for the post, we can create the `@uuid` directive:
    
    ```graphql
    directive @uuid(field: String!) on OBJECT
    ```
    
    You may notice that we used `OBJECT` instead of `FIELD_DEFINITION` because this directive will be applied on GraphQL Types like `Post` type.
    
    ```javascript
    function uuidDirectiveTransformer(schema, directiveName) {
      return mapSchema(schema, {
        // The mapper for OBJECT is OBJECT_TYPE
        [MapperKind.OBJECT_TYPE]: (type) => {
          const uuidDirective = getDirective(schema, type, directiveName)?.[0];
          if (uuidDirective) {
            const { field } = uuidDirective; // Get the directive param
            const config = type.toConfig();
            config.fields[field].resolve = () => crypto.randomUUID();
            return new GraphQLObjectType(config);
          }
        }
      });
    }
    ```
    <span></span>
    ```javascript
    schema = uuidDirectiveTransformer(schema, 'uuid');
    ```
    
    Then we can apply it to the `Post` type as follows:
    
    ```graphql
    type Post @uuid(field: "uuid") { ... }
    ```
    
6. Also, we can implement a directive to validate a string length like post’s `body` field. Let’s call it `@length(min: Int, max: Int)` :
    
    ```graphql
    directive @length(min: Int, max: Int) on FIELD_DEFINITION
    ```
    <span></span>
    ```javascript
    function lengthDirectiveTransformer(schema, directiveName) {
      return mapSchema(schema, {
        [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
          const lengthDirective = getDirective(schema, fieldConfig, directiveName)?.[0];
          if (lengthDirective) {
            const { resolve = defaultFieldResolver } = fieldConfig;
            const { min, max } = lengthDirective;
            fieldConfig.resolve = async function (source, args, context, info) {
              const result = await resolve(source, args, context, info);
              if (min !== undefined && typeof result === 'string' && result.length < min) {
                throw new ApolloError(
                  `The field ${fieldConfig.astNode.name.value} should contain at least ${min} characters`
                );
              }
              if (max !== undefined && typeof result === 'string' && result.length > max) {
                throw new ApolloError(
                  `The field ${fieldConfig.astNode.name.value} shouldn't exceed the max length (${max})`
                );
              }
              return result;
            };
            return fieldConfig;
          }
        }
      });
    }
    ```
    <span></span>
    ```javascript
    schema = lengthDirectiveTransformer(schema, 'length');
    ```
    
    Now, apply it
    
    ```graphql
    body: String! @length(min: 10)
    ```
    
    Try to set `min: 1000` to check the validation.
    

## Conclusion

Simply put, Directives are a very great tool that can be used to enhance your GraphQL API. In this article, we implemented some use cases of directives only to show you the power of directives, and consequentially, you can implement your own ones that fit your own situation.

## Resources

- [GraphQL Spec](https://spec.graphql.org/October2021/#sec-Type-System.Directives)
- [Schema directives](https://www.graphql-tools.com/docs/schema-directives)
- [Directives](https://www.apollographql.com/docs/apollo-server/schema/directives/)
- [GraphQL directives are underrated](https://blog.logrocket.com/graphql-directives-are-underrated/)
- [Field arguments vs. directives in GraphQL](https://blog.logrocket.com/field-arguments-vs-directives-graphql/)