Charly Poly Senior Software Engineer at Double, former Tech Lead at Algolia. I'm passionate about web software engineering, from frontend development to working on APIs, mostly on TypeScript, GraphQL, and Ruby ecosystems.

3 annotations to use in your GraphQL schema

6 min read 1946

The GraphQL logo.

Introduction

GraphQL’s descriptive query language and parallel resolvers architecture has helped many companies like Shopify and Github deliver powerful data APIs to their users.

A lesser-known feature of GraphQL called annotations is now used everywhere by libraries like Apollo or Relay to provide a better developer experience.

This article will introduce you to annotations and guide you through the most common usage for 3 helpful annotations.

What are annotations?

Syntax

Annotations, or directives, that help you follow the official GraphQL specification are an official feature of the GraphQL language.

A directive is easily recognizable by the @ character, as shown below:

<code>
query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    friends @include(if: $withFriends) {
      name
    }
  }
}
</code>

In this example, taken from the official GraphQL documentation, the directive’s name is “include.”

As written in the GraphQL documentation, directives can receive arguments and can be applied to many places in GraphQL documents.

Directive applied to a query field (Apollo client)

<code>
query GetCartItems {
    cartItems @client
}
</code>

Directive applied to a type definition (Apollo Federation)

<code>
type User @key(fields: "id") {
  id: ID!
  username: String!
}
</code>

For simplicity, this article only covers a subset of the directives’ possible locations (mostly operation’s fields).

A complete list of possible locations is available on this excellent Stack Overflow answer.

Purpose

Directives can be seen as GraphQL syntax shorthands to carry additional information to the GraphQL server or client for the execution of the query or mutation.

GraphQL Server @include directive



<code>
query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    friends @include(if: $withFriends) {
      name
    }
  }
}
</code>

The @include directive indicates to the GraphQL server whenever the fields should be included or not in the response data JSON body.

Apollo Client @client directive

<code>
query GetCartItems {
    cartItems @client
}
</code>

Apollo Client is “providing” the @client directive as a shorthand to indicate if a field value should be fetched locally or remotely on a GraphQL API.

We will take a close look at the @client directive in the next section.

How does it work?

Let’s analyze how the @include directive works when applied on the following query:

<code>
query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    friends @include(if: $withFriends) {
      name
    }
  }
}
</code>

Directives.png

When parsing the incoming operation (Hero query), the server navigates through each field, calling the resolver to get the appropriate data.

When reaching the friends field, the GraphQL server encounters the include directive and calls it, along with the field definition and the associated resolver.

The include directive will call the friends resolver only if the if param value is truthful.

To sum up, on the server side, directives “wrap” the field resolvers to:

  • Skip a resolver (example: @skip or @include directives — see next section)
  • Transform the data after calling the resolver

Annotations also allow you to add new behaviors for operations (query, mutations) or adding extra information to types definition.


More great articles from LogRocket:


Let’s now get more practical by reviewing many types of directives, including the standard ones, Apollo custom directives, and community-driven directives.

Universal directives for your applications

We are now familiar with directives syntax and their inner workings. Let’s take a look at some that you’ve probably already used without even noticing.

Standard directives for your schema

The following directives, defined by the GraphQL specifications, are meant to be supported by any GraphQL server (any server library, not only Apollo, that follows the GraphQL spec).

The specification defines three directives, including the @include directive seen in the previous section.

@skip

This directive act as the opposite of the @include directive, including the targeted field, only if the if parameter value is false.

<code>
query Hero($episode: Episode, $onlyHero: Boolean!) {
  hero(episode: $episode) {
    name
    friends @skip(if: $onlyHero) {
      name
    }
  }
}
</code>

@deprecated

Unlike the directive we saw before, this directive is meant to be used on a type definition — not an operation (query/mutation), to provide extra information on the type itself.

<code>
type Hero {
    name: String!
    friends: [Hero!]!
    appearsIn: [Movie!]!
    appearsOn: [Movie!]! @deprecated(reason: "Use `appearsIn`.")
}
</code>

Using this directive to indicate deprecated fields have many advantages:

Modern use-cases of directives on the client side

Apollo Client is making some interesting usage of directives on the client side. Directives on the client side behave a bit differently than server side ones.

How does it work?

Client side directives are only defined on the client side, which means that the targeted GraphQL server is not supposed to receive them.

Directives on the client side.

An extra step not shown in the above graph is that the client side directive execution can happen:

  • Before sending the query to the GraphQL server
  • After getting the response from the GraphQL server and before propagating results to the cache and the component (Hooks)

To sum up, client side directives can interact with cached data and provides features around cache management.

Client-side directives

Let’s now see some examples of features brought by Apollo Client’s client side directives.

Apollo Client @client directive

As seen above, the following GraphQL query is using a client side directive, called @client.

<code>
product(id: $productId) {
  name
  price
  isInCart @client
}
</code>

The @client directive, provided by Apollo Client, indicates that the isInCart field is local, meaning that no request to the GraphQL Server is required to get its value.

As shown in the graph below, our query will be executed in many steps:

  • Apollo Client extracts local fields from the query (@client fields)
  • Get local fields values
  • Request the API (without the @client fields)
  • Cache the remote fields from the API response
  • Return all the fields to the client (or useQuery() Hook)

Queries and caches.

Graph from the official Apollo React Client documentation

Apollo Client has some improvements regarding @client usage. The following query use only local fields and will not trigger a request to the GraphQL server (since there are no remote fields):

<code>
query GetCartItems {
    cartItems @client
}
</code>

The GetCartItems query a perfect example of a “local state” usage of Apollo GraphQL, brought through a delightful experience thanks to directives syntax.

Apollo Client @export directive

Another smart usage that provides a great developer experience is the client side @export directive:

<code>
query CurrentUserPostCount($userId: Int!) {
  currentUserId @client @export(as: "userId")
  postCount(user: $userId)
}
</code>

This directive, provided by Apollo Client, allows you to reuse any local field value (via @client) as a variable for a field, or a subquery of the current operation.

The CurrentUserPostCount query is another example of a smooth experience around state management with GraphQL that Apollo is providing.

Without the @export use, a separated query and added logic would be necessary to achieve the same GraphQL query.

3 powerful annotations for your schema

We saw good examples of directives provided on both the client and the server side by the official specification and major libraries.

Let’s now focus on 3 powerful server side directives that you can start using to supercharge your GraphQL schema.

@computed directive

Provided by the GraphQL Community organization, the @computed directive is particularly handy at solving a ubiquitous problem: repetition and scaffolding.

Many times, in real world applications, some properties are simply computed or derived from existing ones.

Writing resolvers for those computed properties is cumbersome and unnecessary.

Let’s see @computed in action:

<code>
type User {
  firstName: String
  lastName: String
  fullName: String @computed(value: "$firstName $lastName")
}

type Query {
  me: User
}

</code>

Here, the User.fullName property does not require a resolver method. @computed will automatically compute the proper value when requested.

Full installation details are available on the official repository:

GitHub – graphql-community/graphql-directive-computed-property: GraphQL directive for create computed property

The directive allows creating a computed property from fields where is defined. yarn add graphql-directive-computed-property This package requires graphql and graphql-tools as peer dependency Query: Result: Example: admin: String @rest(url: “${URL_TO_API}”) @computed(value: “Are you admin? $admin”) Directive params: The calculated value. It can contain other fields from the type in which it is defined.

Using this directive on your schema will help you keep your resolvers scaling.

Authentication directive

Now let’s tackle a more significant challenge, again with the help of a GraphQL Community organization package: graphql-directive-auth.

The @auth directive provides an elegant solution to the authentication and authorization GraphQL APIs design challenge.

The graphql-directive-auth package is providing two powerful directives:

  • @isAuthenticated
  • @hasRole(role: String)

Both directives apply to the fields definition, as shown on the following schema:

<code>
type Query {
  currentUser: User @isAuthenticated
  users: [User] @isAuthenticated
}

type User {
  id
  first_name
  last_name
  purchases: [Product!]! @hasRole('self', 'admin')
}
</code>

By reading this schema, we understand that:

  • Only authenticated users can access to users (current and listing)
  • User’s purchases can only be listed by
  • Admin
  • The user itself (“self”)

Running the following query is only achievable by an admin, being the only user that has access to all users’ purchases lists:

<code>
query {
  users {
    id
    first_name
    purchases {
      title
      price
    }
  }
}
</code>

graphql-directive-auth is relying on JWT, operating in two modes:

  • The default mode is expecting every request to carry an Authorization header with a JWT Token carrying some role property
  • The custom mode allows you to define a custom method to define how to authenticate users (@isAuthenticated) and also a custom method to define the role checking behavior (hasRole(role)).

This library, thanks to GraphQL directives, provides an easy way to add an extendable authentication layer to your GraphQL API.

Full installation and custom configuration details are available in the official repository:

GitHub – graphql-community/graphql-directive-auth: GraphQL directive for handling auth

The graphql-directive-auth was created to help with common authentication tasks that is faced in almost every API. yarn add graphql-directive-auth We are able to use directives in two different way: To use the default directive behaviour, you need to set APP_SECRET environment variable, and that’s all.

Formatting directive

Most frontend applications make the choice to do all the data formatting on the client side, resulting in kb of third-party formatting libraries.

The @saeris/graphql-directives package provides a set of 30 directives that will allow your frontend to let the API do all the heavy lifting for formatting.

Let’s highlight the most interesting directive:

<code>
type Example {
  # Int | Float => String
  hourlyRate: Int @formatCurrency(
    defaultFormat = "$0,0.00" # String!
    defaultRoundingMode = HALF_AWAY_FROM_ZERO # RoundingMode!
  )
}

query ExampleQuery {
  getPerson {
    # Raw => Default Format => Requested Format
    # 1150 => $11.50 => EUR 11.5
    hourlyRate(format: "USD0,0.0", currency: "EUR") # => EUR 11.5
  }
}
</code>

The @formatCurrency is especially interesting given the complex subject of formatting currencies.

The interesting thing is that, as a type definition directive, @formatCurrency augments the applied field resolver to allow clients to pass optional formatting arguments such as format or currency at query time.

The same goes for dates, numbers, phone numbers formatting, and over 20+ directives for Strings and Measurements!

This package is definitely a must-have for a modern GraphQL API. Take some time to look at the official repository: https://github.com/Saeris/graphql-directives#formatdate

Going further

Directives are a visual and flexible way to extend type definition capabilities and the execution of GraphQL operations (query and mutations).

On the server-side, directives allow you to add metadata to types (@deprecated, Apollo Federation directives), optimizing queries (@include, @skip), and also transforming data (@computed).

On the client side, directives are mainly used to customized cache behavior and add some local state management capabilities (@client).

However, creating useful general purpose directives is complicated.

For this reason, the main power of directives is the ability to write your own in order to:

  • Avoid repeating logic across resolvers and reuse it as directives (validations)
  • Expose custom logic actions to your clients (formatting, custom ACL)

Building directives is achieved by using the SchemaDirectiveVisitor API from graphql-tools.

A good starting point to learn this API is the great Apollo documentation page dedicated to this subject.

Conclusion

I hope that the three annotations recommended in this article will inspire you in writing your own. Please know that directives are just getting started in the GraphQL ecosystem, since many more features are coming in the future @defer, @live, @specifiedBy.

Monitor failed and slow GraphQL requests in production

While GraphQL has some features for debugging requests and responses, making sure GraphQL reliably serves resources to your production app is where things get tougher. If you’re interested in ensuring network requests to the backend or third party services are successful, try LogRocket.LogRocket Dashboard Free Trial Bannerhttps://logrocket.com/signup/

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Charly Poly Senior Software Engineer at Double, former Tech Lead at Algolia. I'm passionate about web software engineering, from frontend development to working on APIs, mostly on TypeScript, GraphQL, and Ruby ecosystems.

Leave a Reply