GraphQL for the JVM: An Introduction to Nodes
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.
Important Notice: Opinions expressed here are the author’s alone. While we're proud of our engineers and employee bloggers, they are not your engineers, and you should independently verify and rely on your own judgment, not ours. All article content is made available AS IS without any warranties. Third parties and any of their content linked or mentioned in this article are not affiliated with, sponsored by or endorsed by American Express, unless otherwise explicitly noted. All trademarks and other intellectual property used or displayed remain their respective owners'. This article is © 2018 American Express Company. All Rights Reserved.