1. Overview
GraphQL is a query language, created by Facebook with the purpose of building client applications based on intuitive and flexible syntax, for describing their data requirements and interactions.
One of the primary challenges with traditional REST calls is the inability of the client to request a customized (limited or expanded) set of data. In most cases, once the client requests information from the server, it either gets all or none of the fields.
Another difficulty is working and maintain multiple endpoints. As a platform grows, consequently the number will increase. Therefore, clients often need to ask for data from different endpoints.
When building a GraphQL server, it is only necessary to have one URL for all data fetching and mutating. Thus, a client can request a set of data by sending a query string, describing what they want, to a server.
2. Basic GraphQL Nomenclature
Let’s have a look at GraphQL’s basic terminology.
- Query: is a read-only operation requested to a GraphQL server
- Mutation: is a read-write operation requested to a GraphQL server
- Resolver: In GraphQL, the Resolver is responsible for mapping the operation and the code running on the backend which is responsible for handle the request. It is analogous to MVC backend in a RESTFul application
- Type: A Type defines the shape of response data that can be returned from the GraphQL server, including fields that are edges to other Types
- Input: like a Type, but defines the shape of input data that is sent to a GraphQL server
- Scalar: is a primitive Type, such as a String, Int, Boolean, Float, etc
- Interface: An Interface will store the names of the fields and their arguments, so GraphQL objects can inherit from it, assuring the use of specific fields
- Schema: In GraphQL, the Schema manages queries and mutations, defining what is allowed to be executed in the GraphQL server
2.1. Schema Loading
There are two ways of loading a schema into GraphQL server:
- by using GraphQL’s Interface Definition Language (IDL)
- by using one of the supported programming languages
Let’s demonstrate an example using IDL:
type User { firstName: String }
Now, an example of schema definition using Java code:
GraphQLObjectType userType = newObject() .name("User") .field(newFieldDefinition() .name("firstName") .type(GraphQLString)) .build();
3. Interface Definition Language
Interface Definition Language (IDL) or Schema Definition Language (SDL) is the most concise way to specify a GraphQL Schema. The syntax is well-defined and will be adopted in the official GraphQL Specification.
For instance, let’s create a GraphQL schema for a User/Emails could be specified like this:
schema { query: QueryType } enum Gender { MALE FEMALE } type User { id: String! firstName: String! lastName: String! createdAt: DateTime! age: Int! @default(value: 0) gender: [Gender]! emails: [Email!]! @relation(name: "Emails") } type Email { id: String! email: String! default: Int! @default(value: 0) user: User @relation(name: "Emails") }
4. GraphQL-java
GraphQL-java is an implementation based on the specification and the JavaScript reference implementation. Note that it requires at least Java 8 to run properly.
4.1. GraphQL-java Annotations
GraphQL also makes it possible to use Java annotations to generate its schema definition without all the boilerplate code created by the use of the traditional IDL approach.
4.2. Dependencies
To create our example, let’s firstly start importing the required dependency which is relying on Graphql-java-annotations module:
<dependency> <groupId>com.graphql-java</groupId> <artifactId>graphql-java-annotations</artifactId> <version>3.0.3</version> </dependency>
We are also implementing an HTTP library to ease the setup in our application. We are going to use Ratpack (although it could be implemented as well with Vert.x, Spark, Dropwizard, Spring Boot, etc.).
Let’s also import the Ratpack dependency:
<dependency> <groupId>io.ratpack</groupId> <artifactId>ratpack-core</artifactId> <version>1.4.6</version> </dependency>
4.3. Implementation
Let’s create our example: a simple API that provides a “CRUDL” (Create, Retrieve, Update, Delete, and List) for users. First, let’s create our User POJO:
@GraphQLName("user") public class User { @GraphQLField private Long id; @GraphQLField private String name; @GraphQLField private String email; // getters, setters, contructors, and helper methods ommited }
In this POJO we can see the @GraphQLName(“user”) annotation, as an indication that this class is mapped by GraphQL along with each field annotated with @GraphQLField.
Next, we’ll create the UserHandler class. This class inherits from the chosen HTTP connector library (in our case, Ratpack) a handler method, which will manage and invoke the GraphQL’s Resolver feature. Thus, redirecting the request (JSON payloads) to the proper query or mutation operation:
@Override public void handle(Context context) throws Exception { context.parse(Map.class) .then(payload -> { Map<String, Object> parameters = (Map<String, Object>) payload.get("parameters"); ExecutionResult executionResult = graphql .execute(payload.get(SchemaUtils.QUERY) .toString(), null, this, parameters); Map<String, Object> result = new LinkedHashMap<>(); if (executionResult.getErrors().isEmpty()) { result.put(SchemaUtils.DATA, executionResult.getData()); } else { result.put(SchemaUtils.ERRORS, executionResult.getErrors()); LOGGER.warning("Errors: " + executionResult.getErrors()); } context.render(json(result)); }); }
Now, the class that will support the query operations, i.e., UserQuery. As mentioned all methods that retrieve data from the server to the client are managed by this class:
@GraphQLName("query") public class UserQuery { @GraphQLField public static User retrieveUser( DataFetchingEnvironment env, @NotNull @GraphQLName("id") String id) { // return user } @GraphQLField public static List<User> listUsers(DataFetchingEnvironment env) { // return list of users } }
Similarly to UserQuery, now we create UserMutation, which will manage all the operations that intend to change some given data stored on the server side:
@GraphQLName("mutation") public class UserMutation { @GraphQLField public static User createUser( DataFetchingEnvironment env, @NotNull @GraphQLName("name") String name, @NotNull @GraphQLName("email") String email) { //create user information } }
It is worth notice the annotations in both UserQuery and UserMutation classes: @GraphQLName(“query”) and @GraphQLName(“mutation”). Those annotations are used to define the query and mutation operations respectively.
With the GraphQL-java server able to run the query and mutation operations, we can use the following JSON payloads to test the request of the client against the server:
- For the CREATE operation:
{ "query": "mutation($name: String! $email: String!){ createUser (name: $name email: $email) { id name email age } }", "parameters": { "name": "John", "email": "john@email.com" } }
As the response from the server for this operation:
{ "data": { "createUser": { "id": 1, "name": "John", "email": "john@email.com" } } }
- For the RETRIEVE operation:
{ "query": "query($id: String!){ retrieveUser (id: $id) {name email} }", "parameters": { "id": 1 } }
As the response from the server for this operation:
{ "data": { "retrieveUser": { "name": "John", "email": "john@email.com" } } }
GraphQL provides features the client can customize the response. So, in the last RETRIEVE operation used as the example, instead of returning the name and email, we can, for instance, return only the email:
{ "query": "query($id: String!){ retrieveUser (id: $id) {email} }", "parameters": { "id": 1 } }
So, the returning information from the GraphQL server will only return the requested data:
{ "data": { "retrieveUser": { "email": "john@email.com" } } }
5. Conclusion
GraphQL is an easy and quite attractive way of minimizing complexity between client/server as an alternative approach to REST APIs.
As always, the example is available at our GitHub repository.