Using Custom Directives with Apollo Federation
Tags: graphql, javascript, apollo
Published: 2020-08-31 19:57:27

In this post, I'm going to walk through how you can add custom directives to implementing services' schemas when using [Apollo Federation](https://www.apollographql.com/docs/apollo-server/federation/introduction/).

Most of what follows in this post has been adapted from various pages in the Apollo documentation, but I thought it would be helpful to consolidate that information as fully-realized demo (with some additional context added for good measure).

The API we'll work with throughout this post is based on one that I built out in [a previous tutorial detailing the basics of Apollo Federation](https://dev.to/mandiwise/getting-started-with-apollo-federation-and-gateway-4739). If you haven't read through that post yet, I encourage you to take a look at it before proceeding (and I especially encourage you to do so if you're new to Apollo Federation). [You can find the complete code from that post here.](https://github.com/mandiwise/basic-apollo-federation-demo)

Do note that in this follow-up tutorial we'll be using updated versions of the following Apollo packages:

- `@apollo/federation@0.18.0`
- `@apollo/gateway@0.18.0`
- `apollo-server@2.16.0`

## Custom Directive Support with a Gateway API

Custom directives are now supported in two different ways with Apollo Federation. We can use both _type system directives_ and _executable directives_.

Type system directives are likely what you're most familiar with if you've used custom directives with Apollo Server before. These directives are applied directly to the schema and can be added in a variety of locations. For example:

```graphql
directive @date(defaultFormat: String = "mmmm d, yyyy") on FIELD_DEFINITION

type Person {
  dateOfBirth: String @date
}
```

Though it may seem counterintuitive at first, according to the Apollo docs the gateway API provides support for type system directive by [stripping them from the composed schema](https://github.com/apollographql/apollo-server/pull/3536). The definitions and uses of any type system directives remain intact in the implementing services' schemas though, so these directives are ultimately managed on a per-service basis.

An executable directive, on the other hand, would be defined in a schema but applied in the operation sent from the client:

```graphql
query {
  person(id: "1") {
    name @allCaps
  }
}
```

Type system directives and executable directives are supported in different locations, so you should take a look at [the GraphQL spec](http://spec.graphql.org/June2018/#sec-Type-System.Directives) for more details on this. For the `@allCaps` directive, we would see in its corresponding schema that it had been applied on the `FIELD` location rather than the `FIELD_DEFINITION` location as the previous example has been.

Executable directives are also handled differently from type system directives at the gateway API level. When working with executable directives, there are [stricter rules about how they are implemented](https://github.com/apollographql/apollo-server/pull/3464) with Apollo Federation. The Apollo docs caution that we must ensure all implementing services define the same set of executable directives. In other words, the executable directives must exist in all implementing services and specify the same locations, arguments, and arguments types (if not, a composition error will occur).

The Apollo documentation also indicates that while executable directives are supported by Apollo Gateway, they are not (currently) supported by a standard Apollo Server. Further, their support in Apollo Gateway is largely intended to be used with implementing services that are not created with Apollo Server. For these reasons, we will be working with type system directives in this tutorial.

## What We're Building

We're going to add a custom `@date` directive much like the one outlined in [this example in the Apollo docs](https://www.apollographql.com/docs/apollo-server/schema/creating-directives/#formatting-date-strings). Our goal will be to create a directive that can be applied to a date field where a default format for that date string can be specified as an argument. 

The `@date` directive definition will look like this:

```graphql
directive @date(defaultFormat: String = "mmmm d, yyyy") on FIELD_DEFINITION
```

This directive will make it possible to take a not-so-human-friendly date string saved in a database and convert it to a format that's a little easier on the eyes when a date-related field is returned from a query. Where the directive is defined, we set a `defaultFormat` for the date string that will be used for the entire implementing service's schema in the event that one isn't provided when the `@date` directive is applied to a specific field.

In practice, if we applied the `@date` directive to field like this...

```graphql
dateOfBirth: String @date
```

...then we would expect to get back a date such as "January 1, 1970" (as specified by the `defaultFormat` argument on the directive) whenever we query this field.

We'll take our demo a step further and provide a `format` argument on a date-related field that can override the `defaultFormat` of the `@date` directive if the client querying the field wishes to do:

```graphql
releaseDate(format: String): String @date
```

Again, the format will be "January 1, 1970" unless the querying client overrides this format by including a `format` argument for this field.

Lastly, we could even combine a `format` field with special `defaultFormat` for the specific field:

```graphql
releaseDate(format: String): String @date(defaultFormat: "d mmmm yyyy")
```

In the example above, we can expect that the date string will use the `format` argument on the field first and will default to the `defaultFormat` specified for the `@date` directive as a fallback (and in this case, the schema-wide `defaultFormat` for the directive will be ignored).

## Create the `@date` Directive

First, we'll need to update the existing `data.js` file in our project to include a `dateOfBirth` field for people and a `releaseDate` field for films. We'll add all of the date values as ISO 8601 strings but we'll transform them into a more readable format with our directive later on:

```js
export const people = [
  {
    id: "1",
    name: "Steven Spielberg",
    dateOfBirth: "1946-12-18T00:00:00+00:00" // NEW!
  },
  {
    id: "2",
    name: "Richard Dreyfuss",
    dateOfBirth: "1947-10-29T00:00:00+00:00" // NEW!
  },
  {
    id: "3",
    name: "Harrison Ford",
    dateOfBirth: "1942-07-13T00:00:00+00:00" // NEW!
  }
];

export const films = [
  {
    id: "1",
    title: "Jaws",
    actors: ["2"],
    director: "1",
    releaseDate: "1975-06-20T00:00:00+00:00" // NEW!
  },
  {
    id: "2",
    title: "Close Encounters of the Third Kind",
    actors: ["2"],
    director: "1",
    releaseDate: "1977-11-15T00:00:00+00:00" // NEW!
  },
  {
    id: "3",
    title: "Raiders of the Lost Ark",
    actors: ["3"],
    director: "1",
    releaseDate: "1981-06-21T00:00:00+00:00" // NEW!
  }
];
```

Next, we'll create a `shared` directory that we'll use to organize the custom directives that we'll reuse across implementing services and we'll also add a file to it called `FormattableDateDirective.js`:

```sh
mkdir shared && touch shared/FormattableDateDirective.js
```

To assist with date string formatting, we'll need to install the dateformat package in our project too:

```sh
npm i dateformat@3.0.3
```

Now we can set up our custom directive. Add the following code to `shared/FormattableDateDirective.js`:

```js
import { defaultFieldResolver, GraphQLString } from "graphql";
import { SchemaDirectiveVisitor } from "apollo-server";
import formatDate from "dateformat";

class FormattableDateDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    // date argument handling code will go here...
  }
}

export default FormattableDateDirective;
```

Above, we can see that Apollo Server provides a handy class called `SchemaDirectiveVisitor` that we can extend to create our custom schema directives. We also need the `defaultFieldResolver` and `GraphQLString` imports from `graphql`, and the `formatDate` function imported from `dateformat`.

We set up our `FormattableDateDirective` by overriding the `visitFieldDefinition` method of the parent `SchemaDirectiveVisitor` class. This method corresponds to the `FIELD_DEFINITION` location we'll apply our custom directive to in the schemas shortly. Now we can implement the date-handling logic inside `visitFieldDefinition`:

```js
// ...

class FormattableDateDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    const { resolve = defaultFieldResolver } = field;
    const { defaultFormat } = this.args;

    field.args.push({
      name: "format",
      type: GraphQLString
    });

    field.resolve = async function (
      source,
      { format, ...otherArgs },
      context,
      info
    ) {
      const date = await resolve.call(this, source, otherArgs, context, info);
      return formatDate(date, format || defaultFormat);
    };
  } // UPDATED!
}

export default FormattableDateDirective;
```

The code we just added to the `visitFieldDefinition` may seem a bit dense at first, but in a nutshell, if the field is queried with a `format` argument, then that date format will be applied to the resolved field value. If the `format` argument doesn't exist, then the `defaultFormat` specified for the `@date` directive will be used (and the `defaultFormat` may be applied at the field level or where the directive is defined in the schema).

## Use the `@date` Directive in the People Service

Next, we'll update `people/index.js` by importing the new custom directive along with `SchemaDirectiveVisitor` from Apollo Server:

```js
import { ApolloServer, gql, SchemaDirectiveVisitor } from "apollo-server"; // UPDATED!
import { buildFederatedSchema } from "@apollo/federation";

import { people } from "../data.js";
import FormattableDateDirective from "../shared/FomattableDateDirective"; // NEW!

// ...
```

We need to import the `SchemaDirectiveVisitor` class in this file as well because we need to add our custom directives to this implementing service's schema in a slightly different way than we would if we were building a vanilla Apollo Server. (We'll see how this is done in just a moment...)

Below the imports, we'll add our custom directive to the schema, add the `dateOfBirth` field, and apply the `@date` directive to it:

```js
// ...

const typeDefs = gql`
  directive @date(defaultFormat: String = "mmmm d, yyyy") on FIELD_DEFINITION # NEW!

  type Person @key(fields: "id") {
    id: ID!
    dateOfBirth: String @date # NEW!
    name: String
  }

  extend type Query {
    person(id: ID!): Person
    people: [Person]
  }
`;

// ...
```

Now we need to let Apollo Server know about the definition of our custom directive. If you've added custom directives to an Apollo Server without federation before, then you're likely familiar with the `schemaDirectives` option that we would set inside of its constructor.

However, instead of setting the `schemaDirectives` option in the `ApolloServer` constructor, we'll refactor our code to call the `visitSchemaDirectives` method on the `SchemaDirectiveVisitor` class and pass in the `schema` and an object containing our directives. Note that we call this function on our schema before passing it into `ApolloServer`:

```js
// ...

const schema = buildFederatedSchema([{ typeDefs, resolvers }]); // NEW!
const directives = { date: FormattableDateDirective }; // NEW!
SchemaDirectiveVisitor.visitSchemaDirectives(schema, directives); // NEW!

const server = new ApolloServer({ schema }); // UPDATED!

server.listen({ port }).then(({ url }) => {
  console.log(`People service ready at ${url}`);
});
```

Let's run `npm run dev` to start up our API now and test it out. Head over GraphQL Playground at [http://localhost:4000/graphql](http://localhost:4000/graphql) and run the following query:

```graphql
query {
  person(id: "1") {
    name
    dateOfBirth
  }
}
```

You should see that the `dateOfBirth` string is in the format specified by our custom directive, rather than in an ISO 8601 format as it is in the mocked data:

```json
{
  "data": {
    "person": {
      "name": "Steven Spielberg",
      "dateOfBirth": "December 17, 1946"
    }
  }
}
```

## Update the Films Service to Use the `@date` Directive

Let's reuse our custom directive in our films service now too. We'll start by importing `SchemaDirectiveVisitor` and the `FormattableDateDirective` into `films/index.js` this time:

```js
import { ApolloServer, gql, SchemaDirectiveVisitor } from "apollo-server"; // UPDATED!
import { buildFederatedSchema } from "@apollo/federation";

import { films } from "../data.js";
import FormattableDateDirective from "../shared/FomattableDateDirective"; // NEW!

// ...
```

Next, we'll add the `@date` directive to this service's type definitions as well and a `releaseDate` field to the `Film` object type. We'll make this field a bit fancier than the `dateOfBirth` field is by adding a `format` argument to the field and specifying a `defaultFormat` for the `@date` directive applied to this field that's different from the `defaultFormat` specified for the schema as a whole:

```js
const typeDefs = gql`
  directive @date(defaultFormat: String = "mmmm d, yyyy") on FIELD_DEFINITION # NEW!

  type Film {
    id: ID!
    title: String
    actors: [Person]
    director: Person
    releaseDate(format: String): String @date(defaultFormat: "shortDate") # NEW!
  }

  # ...
`;

// ...
```

The dateformat package has several [named formats](https://www.npmjs.com/package/dateformat#named-formats) that we can use, so we use the `shortDate` to return a date string in a "01/01/70" format by default. Also, note that despite adding a `format` argument to this query we don't need to modify our resolvers because we handled it in our `FormattableDateDirective` class.

Next, we'll update how we instantiate the `ApolloServer` for the films service just as we did for the people service before:

```js
// ...

const schema = buildFederatedSchema([{ typeDefs, resolvers }]); // NEW!
const directives = { date: FormattableDateDirective }; // NEW!
SchemaDirectiveVisitor.visitSchemaDirectives(schema, directives); // NEW!

const server = new ApolloServer({ schema }); // UPDATED!

server.listen({ port }).then(({ url }) => {
  console.log(`Films service ready at ${url}`);
});
```

Now we can head back over to GraphQL Playground and test out our new and improved schema. Try running the `film` query with the `releaseDate` field:

```graphql
query {
  film(id: "1") {
    title
    releaseDate
  }
}
```

You should see the `releaseDate` formatted as follows:

```json
{
  "data": {
    "film": {
      "title": "Jaws",
      "releaseDate": "6/19/75"
    }
  }
}
```

Now try running a query with `format` argument:

```graphql
query {
  film(id: "1") {
    title
    releaseDate(format: "yyyy")
  }
}
```

And you'll see that the date format specified by the `format` argument overrides the `defaultFormat` that was set in the `@date` directive applied to this field:

```json
{
  "data": {
    "film": {
      "title": "Jaws",
      "releaseDate": "1975"
    }
  }
}
```

## Can Custom Directives Be Use with Extended Types Too?

Yes! We can define a custom directive in an implementing service and apply it to a field for a type that has been extended from another service.

We'll walk through a final example to see this in action. We'll add a new custom directive that can convert a field with a name of `title` to all caps. (I know, it's a bit contrived, but bear with me!)

First, we'll create a new file called `AllCapsTitleDirective.js` in the `shared` directory:

```sh
touch shared/AllCapsTitleDirective.js
```

Next, we'll define our custom directive much as we did before, but this time we'll map over an array of film objects and convert the value of the `title` property to all uppercase letters:

```js
import { defaultFieldResolver } from "graphql";
import { SchemaDirectiveVisitor } from "apollo-server";

class AllCapsTitleDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    const { resolve = defaultFieldResolver } = field;

    field.resolve = async function (...args) {
      const result = await resolve.apply(this, args);

      if (result.length) {
        return result.map(res => ({ ...res, title: res.title.toUpperCase() }));
      }

      return result;
    };
  }
}

export default AllCapsTitleDirective;
```

Next, we'll add our new directive to `films/index.js`:

```js
import { ApolloServer, gql, SchemaDirectiveVisitor } from "apollo-server";
import { buildFederatedSchema } from "@apollo/federation";

import { films } from "../data.js";
import AllCapsTitleDirective from "../shared/AllCapsTitleDirective"; // NEW!
import FormattableDateDirective from "../shared/FomattableDateDirective";

// ...
```

Then we'll add the `@allCapsTitle` to the `directed` field:

```js
// ...

const typeDefs = gql`
  directive @allCapsTitle on FIELD_DEFINITION # NEW!

  directive @date(defaultFormat: String = "mmmm d, yyyy") on FIELD_DEFINITION

  # ...

  extend type Person @key(fields: "id") {
    id: ID! @external
    appearedIn: [Film]
    directed: [Film] @allCapsTitle # UPDATED!
  }

  # ...
`;

// ...
```

Lastly, we'll add the `AllCapsTitleDirective` to the `directives` object that is passed into `SchemaDirectiveVisitor.visitSchemaDirectives`:

```js
// ...

const schema = buildFederatedSchema([{ typeDefs, resolvers }]);
const directives = {
  date: FormattableDateDirective,
  allCapsTitle: AllCapsTitleDirective
}; // UPDATED!
SchemaDirectiveVisitor.visitSchemaDirectives(schema, directives);

// ...
```

Now we can try querying for a single person again:

```graphql
query {
  person(id: 1) {
    name
    directed {
      title
    }
  }
}
```

And we'll see that the titles of the films they directed have been successfully converted to all caps:

```json
{
  "data": {
    "person": {
      "name": "Steven Spielberg",
      "directed": [
        {
          "title": "JAWS"
        },
        {
          "title": "CLOSE ENCOUNTERS OF THE THIRD KIND"
        },
        {
          "title": "RAIDERS OF THE LOST ARK"
        }
      ]
    }
  }
}
```

## Summary

In this post, we added custom directives to a GraphQL API built using Apollo Federation with two implementing services. We were able to reuse a `@date` directive in both services, and we were also able to apply an `@allCapsTitle` directive to a field of a type that was extended from another service.

As I mentioned, much of what I presented in this post was adapted and consolidated from examples in the official Apollo documentation, so you may want to check out these links for further context:

- [Apollo Federation: The gateway – Custom directive support](https://www.apollographql.com/docs/apollo-server/federation/gateway/#custom-directive-support)
- [Apollo Federation: Implementing Services – Defining custom directives](https://www.apollographql.com/docs/apollo-server/federation/implementing-services/#defining-custom-directives)
- [Implementing directives – Examples – Formatting date strings](https://www.apollographql.com/docs/apollo-server/schema/creating-directives/#formatting-date-strings)

You can clone or download [the completed code for this tutorial here](https://github.com/mandiwise/apollo-federation-custom-directives-demo).