abstract highway intersection

GraphQL is supported by many different programming languages for server-side implementations; however the number of client libraries is limited. Most of these client libraries are designed to make it easy to build UI components that fetch data with GraphQL. There is no prevailing client library to invoke GraphQL endpoints using Java. To fix this, we have developed Nodes: a GraphQL client that is suitable for any JVM environment. It is designed for Java engineers with a desire to use GraphQL APIs in a familiar way – a simple, flexible, compatible, adoptable, understandable library for everyone!

Putting GraphQL in the JVM

While the focus of GraphQL usage has been placed on web and mobile interactions, there are many benefits to using GraphQL across all layers of the technology stack, such as providing an abstraction layer joining any number of data sources, creating a flexible and adaptable interface to the data, and allowing consumers to decide how to interpret that data. All of this adds up to a shift in usability toward consumers, giving them more flexibility and increasing the reusability of already exposed resources. Taking advantage of these benefits means giving developers the necessary tools. Many of the existing GraphQL clients rely on developers having a strong knowledge of the GraphQL specification for building queries and embedding them into their requests. Instead, we designed our client with a focus on building the query from existing data models. This allows engineers to quickly and efficiently integrate any JVM application with a GraphQL API without breaking the current architecture.

Nodes

The client we designed currently supports most GraphQL functionality. It is designed to pass in any standard Data Transfer Object (DTO), otherwise known as a value class or data class, and will construct the GraphQL request to match the fields described in the DTO. By default, the only supported field types are the standard java.lang.* and primitives matching the GraphQL specified types as shown below.

GraphQL Scalar Mappings to Java

GraphQL Scalar java.lang.* Java Primitive
Int Integer int
Float Double double
String String -
Boolean Boolean boolean

If custom types are required, there is an option to add them; will be demonstrated in the examples. The library also comes with annotations to decorate the DTO describing field inputs and ignorables. Now let’s get into some examples!

Installing

Maven

<dependency>
  <groupId>io.aexp.nodes.graphql</groupId>
  <artifactId>nodes</artifactId>
  <version>latest</version>
</dependency>

Gradle

compile 'io.aexp.nodes.graphql:nodes:latest'

Now that we have the package, let’s begin constructing a very basic DTO with some getters and setters.

MyRequestObject.java

public class MyRequestObject {
  private String message;

  public String getMessage() {
    return message;
  }

  public void setMessage(String message) {
    this.message = message;
  }
}

Example Request Snippet

GraphQLRequestEntity requestEntity = GraphQLRequestEntity.Builder()
  .url("https://graphql.example.com/graphql")
  .request(MyRequestObject.class)
  .build();
GraphQLResponseEntity<MyRequestObject> responseEntity = graphQLTemplate.query(requestEntity, MyRequestObject.class);
System.out.println(responseEntity.getResponse().getMessage());

This example shows a simple query to the service trying to get a message field in response. So how would this query look in GraphQL?

query {
  MyRequestObject {
    message
  }
}

This is a good first step, but the uppercase naming conventions of class names do not match with the camelCase query I specified, so how do I follow conventions across both projects?

This can be solved with using some of the built-in annotations to provide flexibility and maintain standards across all projects. Simply add the annotation @GraphQLProperty above the className to specify a different query name.

@GraphQLProperty(name="myRequestObject")
public class MyRequestObject {
  ...

The request now becomes:

query {
  myRequestObject {
    message
  }
}

Fantastic! Now we can make the simplest of queries. But let’s say this now takes an argument. What then? Again, this is solved with the same annotation @GraphQLProperty but with another parameter input.

@GraphQLProperty(name="myRequestObject", arguments={ "id" })
public class MyRequestObject {
  ...

By doing so, the request will be updated to query for that ID, adding the lines below to set the arguments in the request builder.

GraphQLRequestEntity requestEntity = GraphQLRequestEntity.Builder()
  .url("https://graphql.example.com/graphql")
  .request(MyRequestObject.class)
  .arguments(new Arguments("myRequestObject", new Argument("id", "d070633a9f9")))
  .build();

The request will now look like this.

query {
  myRequestObject(id: "d070633a9f9") {
    message
  }
}

We have covered the basics of making a query and adding an argument to that query. Now let’s take the example given above with custom date formats for a nested field within the query.

Adding to MyRequestObject.java

...
private ResourceData resourceData;

public ResourceData getMessage() {
  return resourceData;
}

public void setMessage(ResourceData resourceData) {
  this.resourceData = resourceData;
}
...

ResourceData.java

public class ResourceData {
  @GraphQLArguments({
    @GraphQLArgument(name="format"),
    @GraphQLArgument(name="timezone")
  })
  private String timestamp;

  ... getters + setters
}

And setting the values with:

GraphQLRequestEntity requestEntity = GraphQLRequestEntity.Builder()
  .url("https://graphql.example.com/graphql")
  .request(MyRequestObject.class)
  .arguments(new Arguments("myRequestObject.resourceData.timestamp",
    new Argument("format", "MM/DD/YYYY"),
    new Argument("timezone", "EST")))
  .build();

…now makes the query as follows.

query {
  myRequestObject(id: "d070633a9f9") {
    message
    resourceData {
      timestamp(format: "MM/DD/YYYY", timezone: "EST")
    }
  }
}

When using the time arguments, you might have multiple models using the same format. It would be irritating to type that out for all of them. This is a good case for GraphQL variables. These work very similarly to the arguments, but values are defined at the top level. The @GraphQLArguments and @GraphQLArgument will be replaced, respectively, with @GraphQLVariables and @GraphQLVariable. The variables are then set in the requestEntity like this.

GraphQLRequestEntity requestEntity = GraphQLRequestEntity.Builder()
  .url("https://graphql.example.com/graphql")
  .request(MyRequestObject.class)
  .variables(new Variable("format", "MM/DD/YYYY"), new Variable("timezone", "EST"))
  .build();

As mentioned above, the default fields are limited to some of the standard types, but adding support for custom fields is fairly simple. This is in support of the GraphQL implementation of custom scalar values.

Let’s take a look at adding a couple of the most popular types in Java: BigDecimal and Date.

GraphQLRequestEntity requestEntity = GraphQLRequestEntity.Builder()
  .url("https://graphql.example.com/graphql")
  .request(MyRequestObject.class)
  .scalars(BigDecimal.class, Date.class)
  .build();

So far we’ve demonstrated how we can fetch against any data behind a GraphQL interface. Now let’s take a look at how we can create or modify data, as well as introduce the InputObject type for sending nested structures as arguments.

The InputObject also works for query arguments and variables. Below is a full example of creating a new user object, where the AddUser class is only expecting the server to respond with the newly created user ID.

InputObject card = new InputObject.Builder<String>()
  .put("tier", "platinum")
  .put("number", "37XXXXXXXXXX001")
  .build();
InputObject user = new InputObject.Builder<Object>()
  .put("name", "CF Frost")
  .put("card", card)
  .build();

GraphQLRequestEntity requestEntity = GraphQLRequestEntity.Builder()
  .url("https://graphql.example.com/graphql")
  .arguments(new Arguments("addUser", new Argument("user", user)))
  .request(AddUser.class)
  .build();

GraphQLResponseEntity<AddUser> responseEntity = graphQLTemplate.mutate(requestEntity, AddUser.class);
System.out.println(responseEntity.getResponse());

The request will look like this.

mutation {
  addUser(user: {
    name: "CF Frost",
    card: {
      tier: "platinum",
      number: "37XXXXXXXXXX001"
    }
  }) {
    id
  }
}

As you can see in the example, the main difference between querying for data and mutating it is simply calling graphQLTemplate.mutate instead of graphQLTemplate.query. This means that many structures can be reused between both queries and mutations, since all the defined annotations and methods work interchangeably between the two.

Conclusion

GraphQL is an interesting technology that has many uses, not restricted to just one layer of the stack. One of the largest organizations within American Express has adopted and begun using it in production across multiple teams, and introducing my fellow coworkers to and educating them about this technology has been a great and ongoing journey. We are eager to see the developed growth and use of the infrastructure we have built within American Express and are excited to share it with the outside world. Nodes is still in the early development phases, but already is being used in production as you read this! It implements most of the GraphQL specifications today, and we hope to add support for subscriptions and request caching in the future.