Article

Routing in GraphQL

Use custom directives to route data in GraphQL

By

Dan Debrunner

GraphQL is great for when you want to combine all your data into one single API, without having to deal with different response formats. When you're looking to combine data from these different backends, you want to limit the number of calls between these backends to collect all the data you need.

IBM API Connect Essentials (formerly StepZen), added a custom directive called @supplies to GraphQL. You can define a root Query or Mutation field to return an abstract type (interface or union). If the field is not annotated with a backend directive (such as @rest), then it can be resolved indirectly through the @supplies custom directive.

The @supplies custom directive allows you to separate an API from its implementation. For example, an order schema can define an abstract field that provides package tracking without defining which delivery services provide that information. Then, different deployments of the GraphQL schema that uses the same API can provide implementations.

In this article, you learn how to use the @supplies custom directive through an example, where you're implementing GraphQL for different delivery tracking services. Imagine one of these services supports “Fast Package” delivery while another supports “Fast Package” and “Rain or Shine” delivery.

Single supplying field

In this first example, we are going to add tracking information to a customer orders schema.

First, Define a simple interface that contains information about a delivery and a Query field.

interface Delivery {
  when:Date
  note:String
}

extend type Query {
  expected(id:ID!):Delivery
}

Then, extend the Order type to include delivery information by using the @materializer directive.

extend type Order {
  expected:Delivery @materializer(query:"expected" arguments:{name:"id" field:"trackingId"})
}

Now, the schema can be deployed but any selection of Order.expected will return null as there is no implementation for Query.expected. At this point, you can add the @supplies custom directive.

Independently, you need to define a type for the “Fast Package” delivery service.

"""
Delivery by FastPackage.
"""
type FastPackage implements Delivery {
  when: Date
  note: String
  distance: Int
}

FastPackage implements Delivery but has an additional field with information about how far the package is from its destination.

Then, you need to define a field that calls the “Fast Package” REST API and indicates that it supplies Query.delivery:

extend type Query {
   fp(id:ID!):FastPackage
   @supplies(query:"delivery")
   @rest(endpoint:"https://api.fastpackage.net" configuration:"fp")
}

Now when Query.expected or Order.expected is selected the field is resolved automatically by a selection of Query.fp.

A request can access the additional information by using a fragment:

{
  customer(id:1) {
    name
    orders {
      trackingId
      delivery {
        when
        note
        ... on FastPackage {
         distance
        }
      }
    }
}

Multiple supplying fields

But what if you need to support multiple package tracking services? The @supplies custom directive allows multiple "concrete" fields to supply an abstract field, so we can extend our schema further with an additional delivery service called “Rain or Shine.”

"""
Delivery by RainOrShine.
"""
type RainOrShine implements Delivery {
  when: Date
  note: String
  weather: String
}

Again, the RainOrShine interface implements Delivery, but this time it has different information about the weather at the delivery address when the package is expected.

Similar to Query.fp, you need to add Query.ros:

extend type Query {
  ros(id: ID!): RainOrShine
    @supplies(query:"delivery")
    @rest(endpoint:"https://api.ros.io" configuration:"ros")
}

Now, when Query.expected or Order.expected is selected, the field is resolved automatically by a selection of both Query.fp and Query.ros fields, which results in a call to each delivery service's REST API.

A request can access the additional information from each service using fragments:

{
  customer(id:1) {
    name
    orders {
      trackingId
      delivery {
        when
        note
        ... on FastPackage {
         distance
        }
        ... on RainOrShine {
         weather
        }
      }
    }
}

The supplying fields can use any backend type, one could be @rest, one could be @graphql, and two others could use @dbquery.

Because expected is a singleton of Delivery, only one value can be returned and so API Connect Essentials selects the "best" value.

In our example, because a tracking identifier is specific to a service, one call would return a valid object while the other would return null. So, the "best" value would be the valid object and not the null value.

If the interface field instead was a list of the abstract type, then the list will return all non-null values from the supplying fields. If the supplying fields also return lists, then the lists from the supplying fields are combined into a single list.

Routing data

In some cases, calling out to all supplying fields is correct. However, in this delivery example, it is not correct because:

  • Tracking identifiers for one delivery service are sent to its competitors, which results in leaking information.
  • Additional pointless REST API calls are made to delivery services that are not handling the package, which might result in incurring per-call costs.

To address this scenario, you can use the @supplies custom directive, which supports routing by using an if argument with a script that represents a Boolean expression.

To simplify the example, we assume that tracking identifiers have a prefix that indicates the delivery service, so we add an if argument to each of the @supplies directives.

extend type Query {
  fp(id: ID!): FastPackage
    @supplies(
      query:"delivery"
      if: { src: "id.startsWith('FP-')" }
    )
    @rest(endpoint:"https://api.fastpackage.net" configuration:"fp")

  ros(id: ID!): RainOrShine
    @supplies(
      query:"delivery"
      if: { src: "id.startsWith('ROS-')" }
    )
    @rest(endpoint:"https://api.ros.io" configuration:"ros")
}

Now, when Query.expected or Order.expected is selected, only one of Query.fp or Query.ros will be called, depending on the tracking identifier.

If neither routing condition is matched, then no fields are called and null is returned.

The script has access to the field's arguments as variables (id in this case).

Here, the if condition is ECMAScript (default) syntax, but JSONata syntax is also supported.

Summary and next steps

In this article, you learned how to use the @supplies custom directive to route data in GraphQL, such as when you implement tracking for multiple delivery services in a single GraphQL schema.

You can find a complete overview of the code that we used in this article in this GitHub repo.

Questions? We'd love to connect with you on Discord.