Create GraphQL based REST API application using Spring Boot

Introduction

After a long hiatus, Hanbo is coming back with a bang! Recently, I finally picked up where I have left off and decided to do a tutorial on GraphQL. What I want to build is a sample web based application that can use GraphQL to query and retrieve data objects. To be honest, I don't know much about GraphQL. I only know that it is possible to create application with GraphQL support using Spring Boot. I thought this is the best time to learn and evaluate this new concept and see:

  • How hard is it to create such an application.
  • How powerful is this new concept.
  • And how useful would this be for my future projects.

It was pretty miserable when I first started. I thought I will find some samples and replicated, the process would give me some insights. The two I found, one is not compile-able, and the other only has screenshots of the code. That made the learning somewhat difficult. The process is well worth it. And the end product turned out to be pretty good. I will hand in what I have learned in a very detailed tutorial, and I promise the code will compile and run as expected.

Just to put out some disclaimer, I am no expert on this. This tutorial only serves as a starting point for my quest for more knowledge. This tutorial does not guarantee that it can solve the specific problems you the readers may face. Of course, this tutorial is fairly advanced. Please do your best to keep up.

Overall Architecture

The sample application is a RESTFul API based application. It will handle two different request. Both requests have to be HTTP POST with a request body. The request body contains plain text content that specify the GraphQL query that can be parsed by the sample application and response returned.

Creating the sample application with Spring Boot is fairly straight forward. The hard part is wiring the GraphQL functionality into such an application. Like many other concepts or technologies, integration with Spring Boot is fairly easy with GraphQL. There are MVC starter libraries for GraphQL that can be used. This is not the hard part.

The hard part is all the configurations and code changes needed to make this application. The first is the GraphQL definition for the data types and query syntax. It will be a resource packaged into the jar file. Then I need three different classes, one is the controller to handle the client calls, a service layer object that will create the GraphQL data retrieval service. And the DataFetcher factory that will create the data fetcher objects used by the service object.

I will first explain the additional jars I have to include in my Maven POM file to breath GraphQL functionality into my application. This step will be discussed in the next section.

The Maven POM File

I took a standard Maven POM file for a RESTFul project and modified for this new application. In order to make this support GraphQL queries, I just need to add some more dependent jars into the dependencies section. These are the additional jars I have added:

<dependencies>
   <dependency>
      <groupId>com.graphql-java</groupId>
      <artifactId>graphql-java</artifactId>
      <version>18.2</version>
   </dependency>
   <dependency>
      <groupId>com.graphql-java</groupId>
      <artifactId>graphql-java-spring-boot-starter-webmvc</artifactId>
      <version>2021-10-25T04-50-54-fbc162f</version>
   </dependency>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
   </dependency>
</dependencies>

The two jars that have the group id "com.graphql-java" are the new ones I needed for this project.

Next, I will discuss the query type definition file, and how it is being used.

GraphQL Query Types Definition File

The GraphQL query types definition serves two different purposes:

  • It maps the data object types defined in Java (POJO types) to the type which GraphQL can understand. This allows GraphQL to manipulate the query results.
  • It defines the syntax of query so that GrapQL can parse the query and execute, then retrieve and manipulate the results to have desired fields.

All I have to do is creating a file in <project folder>/src/main/java/resources, called "schema.graphql". You can choose any file name you wish, because this file will be used in the Java code, to be located and loaded, as long as you know the file name, you can pass it to the code to do this.

Here is my "schema.graphql" file content:

type Query {
   userProfiles: [ UserProfile ]
   userProfilesByAge(age: Int): [ UserProfile ]
}

type UserCredential {
   userId: ID!
   userCredential: String!
}

type UserAddress {
   userId: ID!
   userAddressLine1: String!
   userAddressLine2: String
   userCity: String!
   userState: String!
   userZipCode: String!
}

type UserDetail {
   userId: ID!
   userAge: Int!
   userFirstName: String!
   userLastName: String!
   userGender: String
}

type UserProfile {
   userId: ID!
   userName: String!
   userEmail: String!
   userActive: Boolean!
   userDetail: UserDetail
   userCredential: UserCredential
   userAddress: UserAddress
}

The content is not hard to understand. The first "type" is called Query, inside there are two different queries. One does not take any input parameter which will return all objects within a collection. The second one takes one input parameter called "age". It will return any objects within the collection that have the same age. Our collection is a list of user profiles, and is stored in memory. Each profile has user id, user name, user email and a flag indicate whether a user is active or not. Then there are three sub types each contains more details about these users. That is all the other types in this file. And they are the exact mappings to the Java types.

For example, the type definition "UserCredential" shown above is the exact mapping to the java class org.hanbo.boot.rest.models.UserCredential. This is the GraphQL code:

type UserCredential {
   userId: ID!
   userCredential: String!
}

This is the java code for the same class:

package org.hanbo.boot.rest.models;

public class UserCredential
{
   private String userId;
   
   private String userCredential;

   public String getUserId()
   {
      return userId;
   }

   public void setUserId(String userId)
   {
      this.userId = userId;
   }

   public String getUserCredential()
   {
      return userCredential;
   }

   public void setUserCredential(String userCredential)
   {
      this.userCredential = userCredential;
   }
}

This is the GraphQL type definition of the type "UserAddress":

type UserAddress {
   userId: ID!
   userAddressLine1: String!
   userAddressLine2: String
   userCity: String!
   userState: String!
   userZipCode: String!
}

And this is java class of the same type:

package org.hanbo.boot.rest.models;

public class UserAddress
{
   private String userId;
   
   private String userAddressLine1;
   
   private String userAddressLine2;
   
   private String userCity;
   
   private String userState;
   
   private String userZipCode;

   public String getUserId()
   {
      return userId;
   }

   public void setUserId(String userId)
   {
      this.userId = userId;
   }

   public String getUserAddressLine1()
   {
      return userAddressLine1;
   }

   public void setUserAddressLine1(String userAddressLine1)
   {
      this.userAddressLine1 = userAddressLine1;
   }

   public String getUserAddressLine2()
   {
      return userAddressLine2;
   }

   public void setUserAddressLine2(String userAddressLine2)
   {
      this.userAddressLine2 = userAddressLine2;
   }

   public String getUserCity()
   {
      return userCity;
   }

   public void setUserCity(String userCity)
   {
      this.userCity = userCity;
   }

   public String getUserState()
   {
      return userState;
   }

   public void setUserState(String userState)
   {
      this.userState = userState;
   }

   public String getUserZipCode()
   {
      return userZipCode;
   }

   public void setUserZipCode(String userZipCode)
   {
      this.userZipCode = userZipCode;
   }
}

This is the GraphQL type definition of the type "UserDetail":

type UserDetail {
   userId: ID!
   userAge: Int!
   userFirstName: String!
   userLastName: String!
   userGender: String
}

This is the java class of the same:

package org.hanbo.boot.rest.models;

public class UserDetail
{
   private String userId;
   
   private String userFirstName;
   
   private String userLastName;
   
   private String userGender;
   
   private int userAge;

   public String getUserId()
   {
      return userId;
   }

   public void setUserId(String userId)
   {
      this.userId = userId;
   }

   public String getUserFirstName()
   {
      return userFirstName;
   }

   public void setUserFirstName(String userFirstName)
   {
      this.userFirstName = userFirstName;
   }

   public String getUserLastName()
   {
      return userLastName;
   }

   public void setUserLastName(String userLastName)
   {
      this.userLastName = userLastName;
   }

   public String getUserGender()
   {
      return userGender;
   }

   public void setUserGender(String userGender)
   {
      this.userGender = userGender;
   }

   public int getUserAge()
   {
      return userAge;
   }

   public void setUserAge(int userAge)
   {
      this.userAge = userAge;
   }
}

Finally, I have the UserProfile data type, and it combines all other three types into one composite data type. Here is the type definition for GraphQL:

type UserProfile {
   userId: ID!
   userName: String!
   userEmail: String!
   userActive: Boolean!
   userDetail: UserDetail
   userCredential: UserCredential
   userAddress: UserAddress
}

The first four properties are primitive types and the last three are the types I have already shown above. And the equivalent java class type for this is shown as:

package org.hanbo.boot.rest.models;

public class UserProfile
{
   private String userId;
   
   private String userName;
   
   private String userEmail;
   
   private boolean userActive;
   
   private UserAddress userAddress;
   
   private UserCredential userCredential;
   
   private UserDetail userDetail;

   public String getUserId()
   {
      return userId;
   }

   public void setUserId(String userId)
   {
      this.userId = userId;
   }

   public String getUserName()
   {
      return userName;
   }

   public void setUserName(String userName)
   {
      this.userName = userName;
   }

   public String getUserEmail()
   {
      return userEmail;
   }

   public void setUserEmail(String userEmail)
   {
      this.userEmail = userEmail;
   }

   public boolean isUserActive()
   {
      return userActive;
   }

   public void setUserActive(boolean userActive)
   {
      this.userActive = userActive;
   }

   public UserAddress getUserAddress()
   {
      return userAddress;
   }

   public void setUserAddress(UserAddress userAddress)
   {
      this.userAddress = userAddress;
   }

   public UserCredential getUserCredential()
   {
      return userCredential;
   }

   public void setUserCredential(UserCredential userCredential)
   {
      this.userCredential = userCredential;
   }

   public UserDetail getUserDetail()
   {
      return userDetail;
   }

   public void setUserDetail(UserDetail userDetail)
   {
      this.userDetail = userDetail;
   }
}

Now we know how to define the query types with GraphQL, where to put the file and how to map the data types between GraphQL and POJOs, it is time to delve into the application design that incorporate GraphQL and Spring Boot RESTFul API into a new kind of web application.

The Java Application Design

In the next three sub sections, I will explain how I put together the pieces needed to incorporate GraphQL into my RESTFul API application. Why three sections? It is easy to break the application design into three different components. Each serves a specific purpose. I will introduce these three components in the bottom up order. The first is a factory class that will provide two separate DataFetcher objects. The data fetcher objects are the objects that executes the query from the request, and returns a collection of matching data elements.

The Data Fetchers Factory

GraphQL has no idea what kind of retrieval that the application supposed to perform, so it provided the interface which developers like me can implement to specify the exact logic. I can do this by implementing the DataFetcher interface. For this sample application, I defined two DataFetcher objects. One is to retrieve all elements of the collection. The other one is to find all user profiles that has the age equals to the age value in the query.

I packed the two DataFetcher object creations into a factory class. Let's see the full source code of this class, then we can get into the detail of how they were created. Here is the class definition:

package org.hanbo.boot.rest.repos;

import java.util.List;
import java.util.stream.Collectors;

import org.hanbo.boot.rest.models.UserProfile;
import org.springframework.stereotype.Component;

import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;

@Component
public class AllUserProfilesFetchersFactory
{
   private DummyDataBank _dataBank;
   
   public AllUserProfilesFetchersFactory(DummyDataBank dataBank)
   {
      _dataBank = dataBank;
   }
   
   public DataFetcher<List<UserProfile>> dataFetcherForAllUserProfiles()
   {
      DataFetcher<List<UserProfile>> retVal = new DataFetcher<List<UserProfile>>() {
         @Override
         public List<UserProfile> get(DataFetchingEnvironment environment) {
            System.out.println("data fetcher for all user profiles retrieval.");
            return _dataBank.getAllUserProfiles();
         }
     };
     
     return retVal;
   }
   
   public DataFetcher<List<UserProfile>> dataFetcherForUserProfilesByAge()
   {
      DataFetcher<List<UserProfile>> retVal = new DataFetcher<List<UserProfile>>() {
         @Override
         public List<UserProfile> get(DataFetchingEnvironment environment) {
            int expectedAge = (int)environment.getArgument("age");
            System.out.println("data fetcher for user profiles based by age retrieval. Age: " + expectedAge);
            List<UserProfile> retVal = 
            _dataBank.getAllUserProfiles()
                     .stream()
                     .filter( x -> x.getUserDetail() != null && x.getUserDetail().getUserAge() == expectedAge)
                     .collect(Collectors.toList());
            return retVal;
         }
     };
     
     return retVal;
   }
}

This class has two method, the first is to create a DataFetcher object that GraphQL can use to process the full list of all UserProfile objects in the collection which we are querying. Here it is:

public DataFetcher<List<UserProfile>> dataFetcherForAllUserProfiles()
{
   DataFetcher<List<UserProfile>> retVal = new DataFetcher<List<UserProfile>>() {
      @Override
      public List<UserProfile> get(DataFetchingEnvironment environment) {
         System.out.println("data fetcher for all user profiles retrieval.");
         return _dataBank.getAllUserProfiles();
      }
   };
   
   return retVal;
}

In this method, I creates an object of an anonymous object of type DataFetcher. It implements the interface DataFetcher, all I needed is to implement the get() method. And it returns a list of object of type UserProfile. For this one I like to return all the objects that is in my data bank object. Then GraphQL will process the list and return a list of objects that contains only the properties user has specified to be returned.

The second DataFetcher object is similar to the one I just reviewed. It has an addition. The query needs an input parameter called "age". This new DataFetcher object should be able to retrieve the parameter and use it for the actual query. This is done by using the DataFetchingEnvironment parameter passed into the get() method. The DataFetchingEnvironment object has an method called getArgument() that can return the value of the argument that was in the query text. It is stores as type Object, so I need to converted via the explicit conversion. Here it is:

public DataFetcher<List<UserProfile>> dataFetcherForUserProfilesByAge()
{
   DataFetcher<List<UserProfile>> retVal = new DataFetcher<List<UserProfile>>() {
      @Override
      public List<UserProfile> get(DataFetchingEnvironment environment) {
         int expectedAge = (int)environment.getArgument("age");
         System.out.println("data fetcher for user profiles based by age retrieval. Age: " + expectedAge);
         List<UserProfile> retVal = 
         _dataBank.getAllUserProfiles()
            .stream()
            .filter( x -> x.getUserDetail() != null && x.getUserDetail().getUserAge() == expectedAge)
            .collect(Collectors.toList());
         return retVal;
      }
   };
   
   return retVal;
}

The get() method both classes uses streams and query against the collection. This should be pretty easy to understand. It is like C# Linq syntax.

The next section will be the most important part of this tutorial. In it, I will show how to configure the GraphQL so that it can be used by the Rest Controller later on.

The GraphQL Service

It is not really a service, I created the service because it is my habit to do so. The following code is actually initialize the GraphQL object, having the GraphQL to parse the schema file I have created, and configure the two DataFetcher objects to handle the query. Here is the service object type I have created:

package org.hanbo.boot.rest.services;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;

import javax.annotation.PostConstruct;

import org.hanbo.boot.rest.repos.AllUserProfilesFetchersFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;

import graphql.GraphQL;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;

@Service
public class GraphQLProvider
{
   private GraphQL _graphQL;
   
   private AllUserProfilesFetchersFactory _dataFetcherFactory;
   
   public GraphQLProvider(AllUserProfilesFetchersFactory dataFetcherFactory)
   {
      _dataFetcherFactory = dataFetcherFactory;
   }
   
   @Bean
   public GraphQL graphQL() {
       return _graphQL;
   }

   @PostConstruct
   public void init() throws IOException
   {
      ClassPathResource resLoader = new ClassPathResource("schema.graphql");
      InputStream inStr = resLoader.getInputStream();
      Reader typeReader = new InputStreamReader(inStr);
      
      TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(typeReader);
      RuntimeWiring runtimeWiring = buildWiring();
      SchemaGenerator schemaGenerator = new SchemaGenerator();
      GraphQLSchema schema = schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
   
      _graphQL = GraphQL.newGraphQL(schema).build();
   }

   private RuntimeWiring buildWiring() {
      return RuntimeWiring.newRuntimeWiring()
            .type("Query", typeWiring->typeWiring.dataFetcher("userProfiles", _dataFetcherFactory.dataFetcherForAllUserProfiles()))
            .type("Query", typeWiring->typeWiring.dataFetcher("userProfilesByAge", _dataFetcherFactory.dataFetcherForUserProfilesByAge())).build();
   }
}

Let me just explain, piece by piece. The first part is:

private AllUserProfilesFetchersFactory _dataFetcherFactory;

public GraphQLProvider(AllUserProfilesFetchersFactory dataFetcherFactory)
{
   _dataFetcherFactory = dataFetcherFactory;
}

I declare the AllUserProfilesFetchersFactory object as part of this class. Then the constructor will do the dependency injection to instantiate this object. It can be used in the latter part of the class. The next piece is this init() method:

@PostConstruct
public void init() throws IOException
{
   ClassPathResource resLoader = new ClassPathResource("schema.graphql");
   InputStream inStr = resLoader.getInputStream();
   Reader typeReader = new InputStreamReader(inStr);
   
   TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(typeReader);
   RuntimeWiring runtimeWiring = buildWiring();
   SchemaGenerator schemaGenerator = new SchemaGenerator();
   GraphQLSchema schema = schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);

   _graphQL = GraphQL.newGraphQL(schema).build();
}

First, I used annotation @PostConstruct. What is cool about this annotation is that this method will be invoked automatically after the constructor is called. The annotation is not part of Spring, but part of the Java language. In this method, I will first pass in the class path of the schema.graphql file to construct a ClassPathResource constructor and instantiate the resource loader. Then I create an InputStream object and an InputStreamReader object:

ClassPathResource resLoader = new ClassPathResource("schema.graphql");
InputStream inStr = resLoader.getInputStream();
Reader typeReader = new InputStreamReader(inStr);

The next part is to create an object of GrapQLSchema. First, I have to create an object of type TypeDefinitionRegistry. Next, I have to create an object of type RuntimeWiring. This is done by calling method buildWiring(). buildWiring() is my method for associating my two DataFetcher objects to GraphQL. At last, I create an object of type SchemaGenerator and use that to create an object of GrapQLSchema:

TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(typeReader);
RuntimeWiring runtimeWiring = buildWiring();
SchemaGenerator schemaGenerator = new SchemaGenerator();
GraphQLSchema schema = schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);

Here is my buildWiring() method. This is how I associate the schema with the two DataFetcher objects. If you check my schema, there is the type "Query" and, and in it two different queries. One is called "userProfiles"; the other one is called "userProfilesByAge":

private RuntimeWiring buildWiring() {
   return RuntimeWiring.newRuntimeWiring()
         .type("Query", typeWiring->typeWiring.dataFetcher("userProfiles", _dataFetcherFactory.dataFetcherForAllUserProfiles()))
         .type("Query", typeWiring->typeWiring.dataFetcher("userProfilesByAge", _dataFetcherFactory.dataFetcherForUserProfilesByAge())).build();
}

This is pretty simple. Yet, putting all that together by reading the two tutorials and figure out how the components works is a bit hard. At last, I will show you how to use this service object to process the query. All is done in the Rest API controller. I will cover that in the next sub section.

The Rest API Controller

Now that the most important part of the tutorial is done, it is time to use it. The Rest API controller is quite simple:

package org.hanbo.boot.rest.controllers;

import java.io.IOException;
import java.util.Map;

import org.hanbo.boot.rest.services.GraphQLProvider;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.fasterxml.jackson.databind.ObjectMapper;

import graphql.ExecutionResult;

@RestController
public class SampleGraphQLApiController
{
   private GraphQLProvider _queryProvider;
   
   public SampleGraphQLApiController(GraphQLProvider queryProvider)
   {
      _queryProvider = queryProvider;
   }
   
   @PostMapping(value="/api/allUserProfiles")
   public String allUserProfiles(
      @RequestBody
      String queryVal
   ) throws IOException
   {
      ExecutionResult result = _queryProvider.graphQL().execute(queryVal);
      Map<String, Object> resp = (Map<String, Object>) result.getData();
      String retVal = new ObjectMapper().writeValueAsString(resp);
      
      return retVal;
   }
}

The constructor of this class is used to autowire the service object which I have shown in the previous subsection. There is only one other method, which is the action method that handles user request. Before I go into the detail. I want to explain what the request looks like. GraphQL uses a special syntax. So it is not HTML, XML or JSON that it expecting. I choose to use plain text as the request media type. The request content would be a simple string.

The method has only 4 lines of code, The first is to call my service object to return an object of type GraphQL. Then it invokes its method called execute(), which will parse the string and use one of the DataFetcher objects to actually query my collection of UserProfile. For each of the result user profile, additional processing will be done to choose only user defined fields to be returned. So the user profile objects will not be the full list of all the properties, and would be greatly simplified. And the objects are Map objects with keys of string, and values of type Object. Once I got that, I will use the Jackson framework and turn the object into a JSON object and returned to the user.

That is everything about the Java code of this sample application. Now that we have seen all the technical details. It is time to test and see how the application actually works.

How to Run the Sample Application

First, please use the following command on the command line to build the application:

mvn clean install

Once the build succeeds (and it will), you can use the following command to run the application:

java -jar <project folder>/target/hanbo-graphql-restapi-1.0.1.jar

This is the URL for the RESTFul API:

http://localhost:8080/api/allUserProfiles

You can use PostMan or any other RESTFul test application to test this. I used Advanced REST Client (ARC) for testing. It is less advanced than PostMan, but is a good alternative. This is a screenshot of Advanced REST Client (ARC) running on my desktop:

Before we get to the actual testing of the query, I want to share the data collection. In this data collection, there is only one user profile objected added in the list:

package org.hanbo.boot.rest.repos;

import java.util.ArrayList;
import java.util.List;

import org.hanbo.boot.rest.models.UserAddress;
import org.hanbo.boot.rest.models.UserCredential;
import org.hanbo.boot.rest.models.UserDetail;
import org.hanbo.boot.rest.models.UserProfile;
import org.springframework.stereotype.Component;

@Component
public class DummyDataBank
{
   private static List<UserProfile> mockUserProfiles;
   
   static
   {
      mockUserProfiles = new ArrayList<UserProfile>();
      
      UserProfile profileToAdd = new UserProfile();
      
      profileToAdd.setUserId("00000001");
      profileToAdd.setUserName("testUser1");
      profileToAdd.setUserEmail("testUser1@test.org");
      profileToAdd.setUserActive(true);

      UserAddress userAddress = new UserAddress();
      userAddress.setUserAddressLine1("123 Main St.");
      userAddress.setUserAddressLine2("");
      userAddress.setUserCity("Kalamazoo");
      userAddress.setUserId("00000001");
      profileToAdd.setUserAddress(userAddress);
      
      UserDetail userDetail = new UserDetail();
      userDetail.setUserAge(35);
      userDetail.setUserFirstName("Lee");
      userDetail.setUserLastName("Junkie");
      userDetail.setUserGender("M");
      userDetail.setUserId("00000001");
      profileToAdd.setUserDetail(userDetail);

      UserCredential userCredential = new UserCredential();
      userCredential.setUserCredential("xxxxyyyyxxxxyyyy");
      userCredential.setUserId("00000001");
      profileToAdd.setUserCredential(userCredential);
      
      mockUserProfiles.add(profileToAdd);
   }
   
   public List<UserProfile> getAllUserProfiles()
   {
      return mockUserProfiles;
   }
}

This UserProfile type is not extremely complex. In real world examples, a complex object can have layers and layers of data, and in extreme cases, the data size of a single object can have 100+ MB, and can be even bigger.

When we retrieve such an object, it is not necessary to get all the data, only the values we wanted. And we do not want to create a lot of sub types with smaller set of properties. This shifts the problem of a large and complex object type into many smaller size object types. GraphQL solves both problem by dynamically cherry pick the properties of a particular objects and create a new object in a list to be returned. That is what is cool about GraphQL. Now we can see this in the sample test run.

Say that we want to return all the user profiles, and we only want to cherry pick a few properties from:

  • userId from the UserProfile object.
  • userId and userAddressLine1 from the UserAddress object within the UserProfile.
  • userFirstName and userAge from the UserDetail object within the UserProfile.

To do that, all I need to do is create a HTTP Post request against the REST API server, for the request content, the content type is set to "text/plain;charset=UTF-8". The request body would be this:

{
    userProfiles
    {
        userId
        userAddress
        {
            userId
            userAddressLine1
        }
        userDetail
        {
            userFirstName
            userAge
        }
    }
}

When the appllication runs correctly, the return response would be:

{
   "userProfiles": [
      {
         "userId":"00000001",
         "userAddress": {
            "userId":"00000001",
            "userAddressLine1":"123 Main St."
         },
         "userDetail": {
            "userFirstName":"Lee",
            "userAge":35
         }
      }
   ]
}

We can try the second scenario, where we are only interested in user profile whose age is at 35. I can modify my query as the following:

{
    userProfilesByAge(age: 35)
    {
        userId
        userAddress
        {
            userId
            userAddressLine1
        }
        userDetail
        {
            userFirstName
            userAge
        }
    }
}

When I sent this request against the server, I get the same response back. And if I change the age to 34, I will get a response like this:

{
    "userProfilesByAge":[]
}

This means there is no elements in the collection that matches this search criteria. If I mis-typed a property name in the query, for example, like this:

{
    userProfilesByAge(age: 35)
    {
        userId
        userAddress
        {
            userId
            userAddressLine1
        }
        userDetail
        {
            userFirstname
            userAge
        }
    }
}

I would get a response like this:

null

In the back end log, I will see this being output:

022-07-12 11:43:55.851  WARN 956647 --- [nio-8080-exec-1] notprivacysafe.graphql.GraphQL           : Query did not validate : '{
    userProfilesByAge(age: 34)
    {
        userId
        userAddress
        {
            userId
            userAddressLine1
        }
        userDetail
        {
            userFirstname
            userAge
        }
    }
}'

If you compiled the code and are able to run all these example tests, you have succeeded setting up your first GraphQL sample application. I was very happy to see this working the first time I got the expected results. I hope all will go well for you too.

Summary

This has been a fun tutorial to write about. It took me some time to figure out how to design such an application, and how it operates. It was pretty awesome seeing it working for the first Time. I am very grateful that the two tutorials I saw provided all the information I needed to get this to work.

In this tutorial, I have covered the following information:

  • How to define the GraphQL schema and add the schema file to a Sping boot application.
  • How to create customized DataFetcher objects that works for GraphQL.
  • How to configure and create a GraphQL object with the schema and Data Fetcher objects attached.
  • How to create a RESTFul API controller to have raw GraphQL query and return the response.
  • Finally, how to test the sample application.

I also want to summarize the things I learned. GraphQL can be used to simplify the process of creating many different types of sub objects from a common complex data object. If I want to avoid defining too many little and similar objects. This is a great way to dynamically generate these small objects without creating the object type. Overall, it would save me lots of type of creating these smaller data object types. I don't like the idea that I have to create a query schema for the Java object I have defined. It is like defining the same thing twice. Also I have to define the query type as well as implement the query logic. This also seems to be duplicated effort. I guess these are the inconvenience of working with GraphQL, it wouldn't know what we want unless we explicitly define the behaviors.

Please enjoy. I hope this is going to help with your development pursuit. Thank you for reading this tutorial.

History

07/13/2022 - Initial Draft.

Your Comment


Required
Required
Required

All Related Comments

Loading, please wait...
{{cmntForm.errorMsg}}
{{cmnt.guestName}} commented on {{cmnt.createDate}}.

There is no comments to this post/article.