Enable Javascript

Please enable Javascript to view website properly

Toll Free 1800 889 7020

Looking for an Expert Development Team? Take 2 weeks Free Trial! Try Now

GRAPHQL WITH SPRING BOOT

1. Introduction

GraphQL is a new concept devised by Facebook to design Web APIs. It is billed to be an alternative to widely used REST APIs.

Following are the major highlights of graphQL:

1. Home Page : https://graphql.org/

2. Github Repo : https://github.com/facebook/graphql

3. Predictable results : Send a GraphQL query to your API and get exactly what you need, nothing more and nothing less. GraphQL queries always return predictable results.

4. Unlike REST (where we have a end points per resource), application using graphQL exposes a single endpoint. Since GraphQL APIs are organized in terms of types and fields and not endpoints, it provides far more flexibility than traditional REST API.

5. GraphQL queries handle not just the attribute of one source but also simply follow source between them. GraphQL enables us to fetch data from nested data set in a single request (Eg: Get All Posts + latest comments associated with each post). This is achieved in REST, by calling multiple endpoints (1 endpoint to fetch posts + 1 endpoint to fetch comments from each post. This leaves us with hazardous n+1 fetching problem).

Before we dive further into GraphQL, let's discuss REST and the problems with it which GraphQL attempts to solve.

1.1 RESTful API

The Representational State Transfer (REST) architecture is the most popular way to expose server data over web.

In the RESTful architectural style, servers only offer resources. Resources are conceptual things about which clients and servers communicate (Eg User, ShoppingItem, Reviews, ShoppingCart are common resources in a typical shopping portal application). Each resource is identified by a URL.

REST uses standard HTTP verbs are used to perform actions on resources.

  • 1. GET - retrieve a resource
  • 2. PUT - update a resource
  • 3. POST - create a new resource
  • 4. DELETE - remove a resource

In RestFul API, resources can be represented in number of formats. Popular formats being Json, XML, RSS.

1.2 Problems with Restful API:

Under and Over Fetching data : In RESTful API, client has no control on the amount of data server sends for a resource, it can only ask for the resource to server.

Over Fetching means the client is retrieving data that is actually not needed at the moment when it’s being fetched. (Eg : while fetching User details, we might not want his address, but with REST we have no control on it)

Under Fetching is the opposite of overfetching and means that not enough data is included in an API response. (Eg : Get All Posts + latest comments associated with each post). This is achieved in REST, by calling multiple endpoints (1 endpoint to fetch posts + 1 endpoint to fetch comments from each post. This leaves us with hazardous n+1 fetching problem)

1.3 GraphQL Server

GraphQL solves this issue, as control rests with API client as what data it actually needs from server. It asks for specific fields and server happily obliges.

At the heart of any GraphQL implementation is graphQL schema, which is typically description of types of objects, relationships between them and further operations permitted on them (queries and mutations).

type Human { id: String name: String homePlanet: String }

Queries are commonly sent over HTTP to a specific server endpoint (unlike a REST architecture, where there are various endpoints for different solutions).

Type `Query` is used to expose the query operations on our schema

type Query { human(id: String!): Human } The query sent to graphQL server is POST /graphql?query={ human(id: "1") {id, name } } This will return Human type with id=1. Of these only attributes returned are id and name

Type ‘Mutation’ is typically used to modify the server side resources.

type Mutation { addHuman(name: String!) : Human! } Similar to Query we can even define what fields we need from the return type of mutation POST /graphql?mutation={ addHuman(name: "xyz") {id, name } } This will add a human resource as well as return it. Of these only attributes returned are id and name

2. GraphQL Server Application :

Lets try to build a simple API using Spring Boot and GraphQL

  • 1. In our application the core entities are Article and Feedback. An article is similar to a user post and feedback are the replies/comments on it. Thus an article has many feedbacks.
  • 2. Basic operations are get articles, feedbacks for an article, create article and create a feedback.
  • 3. We are using MongoDb as the persistence layer for our application.
  • 4. We will be using Spring boot starter for graphQL java implementation.

2.1 Application HLD

spring

2.2 Project Structure

We will be creating a spring-boot maven project.

The project layout is as follows :

└───maven-project ├───pom.xml └───src ├───main │ ├───java (java source files) │ ├───resources (properties files)

The project structure screenshot is as follows :

spring

2.3 Application Code

2.3.1 Maven Dependencies :

At the very beginning our project has only pom.xml, with following content:

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.hemant.graphql</groupId> <artifactId>spring-boot-graphql</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>spring-boot-graphql</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.1.RELEASE</version> <relativePath /> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> <dependency> <groupId>com.graphql-java</groupId> <artifactId>graphql-spring-boot-starter</artifactId> <version>4.0.0</version> </dependency> <dependency> <groupId>com.graphql-java</groupId> <artifactId>graphql-java-tools</artifactId> <version>4.3.0</version> </dependency> <dependency> <groupId>com.graphql-java</groupId> <artifactId>graphiql-spring-boot-starter</artifactId> <version>4.0.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

Explanation :

  • graphql-spring-boot-starter - autoconfigure a GraphQL Servlet at /graphql
  • graphql-java-tools - to parse GraphQL schemas
  • graphiql-spring-boot-starter - we can use GraphiQL, an Electron app that you can use to test your GraphQL endpoint (/graphiql)
  • spring-boot-starter-web - for enabling web MVC nature of spring boot app
  • spring-boot-starter-data-mongodb - for interacting with MongoDB which is our persistence layer

2.3.2 Model classes :

As explained earlier, we have 2 model classes : Article and Feedback. An article can have multiple feedbacks (thus 1: many) relationship between them.

packagecom.hemant.graphql.model; importjava.util.Date; publicclassArticle { private String id; private String name; private String createdByUserId; private Date createdOn; private Date lastUpdatedOn; public String getId() { return id; } publicvoidsetId(String id) { this.id = id; } public String getName() { return name; } publicvoidsetName(String name) { this.name = name; } public String getCreatedByUserId() { returncreatedByUserId; } publicvoidsetCreatedByUserId(String createdByUserId) { this.createdByUserId = createdByUserId; } public Date getCreatedOn() { returncreatedOn; } publicvoidsetCreatedOn(Date createdOn) { this.createdOn = createdOn; } public Date getLastUpdatedOn() { returnlastUpdatedOn; } publicvoidsetLastUpdatedOn(Date lastUpdatedOn) { this.lastUpdatedOn = lastUpdatedOn; } @Override publicinthashCode() { finalint prime = 31; int result = 1; result = prime * result + ((id == null) ? 0 : id.hashCode()); return result; } @Override publicbooleanequals(Object obj) { if (this == obj) returntrue; if (obj == null) returnfalse; if (getClass() != obj.getClass()) returnfalse; Article other = (Article) obj; if (id == null) { if (other.id != null) returnfalse; } elseif(!id.equals(other.id)) returnfalse; returntrue; } @Override public String toString() { return "Article [id=" + id + ", name=" + name + ", createdByUserId=" + createdByUserId + "]"; } } packagecom.hemant.graphql.model; importjava.util.Date; publicclassFeedback { private String id; private String feedbackText; private String articleId; private String createdByUserId; private Date createdOn; private Date lastUpdatedOn; public String getId() { return id; } publicvoidsetId(String id) { this.id = id; } public String getFeedbackText() { returnfeedbackText; } publicvoidsetFeedbackText(String feedbackText) { this.feedbackText = feedbackText; } public String getArticleId() { returnarticleId; } publicvoidsetArticleId(String articleId) { this.articleId = articleId; } public String getCreatedByUserId() { returncreatedByUserId; } publicvoidsetCreatedByUserId(String createdByUserId) { this.createdByUserId = createdByUserId; } public Date getCreatedOn() { returncreatedOn; } publicvoidsetCreatedOn(Date createdOn) { this.createdOn = createdOn; } public Date getLastUpdatedOn() { returnlastUpdatedOn; } publicvoidsetLastUpdatedOn(Date lastUpdatedOn) { this.lastUpdatedOn = lastUpdatedOn; } @Override publicinthashCode() { finalint prime = 31; int result = 1; result = prime * result + ((id == null) ? 0 : id.hashCode()); return result; } @Override publicbooleanequals(Object obj) { if (this == obj) returntrue; if (obj == null) returnfalse; if (getClass() != obj.getClass()) returnfalse; Feedback other = (Feedback) obj; if (id == null) { if (other.id != null) returnfalse; } elseif (!id.equals(other.id)) returnfalse; returntrue; } @Override public String toString() { return"Feedback [id=" + id + ", feedbackText=" + feedbackText + ", articleId=" + articleId + ", createdByUserId=" + createdByUserId + "]"; } }

In addition to it, we have also defined an enum for SortOrder.

packagecom.hemant.graphql.model.pagination; publicenumSortOrder { ASC, DESC; }

2.3.3 GraphQL Schema :

We have devised our graphQL schema complimenting our models.

Also in schema we have defined the graphQL query and mutations.

The schema is defined as article.graphqls file in src/main/resources folder.

schema { query: Query mutation: Mutation } enumSortOrder { ASC DESC } type Article { id: String name: String createdByUserId: String createdOn: String lastUpdatedOn: String } type Feedback { id: String feedbackText: String articleId: String createdByUserId: String createdOn: String lastUpdatedOn: String } type Query { getAllArticles(pageNumber: Int!, pageSize : Int!, sortOrder: SortOrder!, sortBy: String!): [Article] getFeedBacksForArticle(articleId: String!): [Feedback] } type Mutation { createArticle(name: String!, createdByUserId: String!): Article createNewFeedback(feedbackText: String!, articleId: String!, createdByUserId: String!): Feedback }

2.3.4 Application properties file :

Spring boot gets its properties from default application.properties file in src/main/resources directory.

spring.application.name=spring-boot-graphql server.port = 8080 logging.pattern.console=%d{HH:mm:ss} %-5level %logger{10} - %msg%n logging.level.org.springframework=INFO #mongoDB app.mongodb.host=localhost app.mongodb.port=27017 app.mongodb.database=graphql-demo app.mongodb.username=root app.mongodb.password=root app.mongodb.authdb=admin #GraphQL Properties graphql.servlet.mapping=/graphql graphql.servlet.enabled=true graphql.servlet.corsEnabled=false

2.3.5 QueryResolver class

For all the queries defined in `Query` type in article.graphqls file, this class will provide its implementation.

type Query { getAllArticles(pageNumber: Int!, pageSize : Int!, sortOrder: SortOrder!, sortBy: String!): [Article] getFeedBacksForArticle(articleId: String!): [Feedback] } packagecom.hemant.graphql.resolvers; importjava.util.List; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.stereotype.Component; importcom.coxautodev.graphql.tools.GraphQLQueryResolver; importcom.hemant.graphql.model.Article; importcom.hemant.graphql.model.Feedback; importcom.hemant.graphql.model.pagination.SortOrder; importcom.hemant.graphql.service.ArticleService; @Component publicclassQueryimplementsGraphQLQueryResolver { @Autowired privateArticleServicearticleService; public List<Article>getAllArticles(intpageNumber, intpageSize, SortOrdersortOrder, String sortBy) { returnarticleService.getAllArticles(pageNumber, pageSize, sortOrder, sortBy); } public List<Feedback>getFeedBacksForArticle(String articleId) { returnarticleService.getFeedbacksForArticle(articleId); } }

2.3.6 Mutation Resolver :

For all the mutations defined in `Mutation` type in article.graphqls file, this class provides its implementation.

type Mutation { createArticle(name: String!, createdByUserId: String!): Article createNewFeedback(feedbackText: String!, articleId: String!, createdByUserId: String!): Feedback } packagecom.hemant.graphql.resolvers; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.stereotype.Component; importcom.coxautodev.graphql.tools.GraphQLMutationResolver; importcom.hemant.graphql.model.Article; importcom.hemant.graphql.model.Feedback; importcom.hemant.graphql.service.ArticleService; @Component publicclassMutationimplementsGraphQLMutationResolver { @Autowired privateArticleServicearticleService; public Article createArticle(String name, String createdByUserId) { returnarticleService.createArticle(name, createdByUserId); } public Feedback createNewFeedback(String feedbackText, String articleId,StringcreatedByUserId) { returnarticleService.createFeedback(feedbackText, articleId, createdByUserId); } }

2.3.7 Service Layer

This layer defines the business rules/logic.

packagecom.hemant.graphql.service; importjava.util.List; importcom.hemant.graphql.model.Article; importcom.hemant.graphql.model.Feedback; importcom.hemant.graphql.model.pagination.SortOrder; publicinterfaceArticleService { List<Article>getAllArticles(intpageNumber, intpageSize, SortOrdersortOrder, String sortBy); Article createArticle(String name, String createdByUserId); List<Feedback>getFeedbacksForArticle(String articleId); Feedback createFeedback(String feedbackText, String articleId, String createdByUserId); } packagecom.hemant.graphql.service; importstatic org.apache.commons.lang3.StringUtils.isBlank; importstatic org.apache.commons.lang3.StringUtils.isNotBlank; importjava.util.ArrayList; importjava.util.Date; importjava.util.List; import org.apache.commons.lang3.StringUtils; importorg.bson.types.ObjectId; import org.slf4j.Logger; import org.slf4j.LoggerFactory; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.stereotype.Service; importorg.springframework.util.CollectionUtils; importcom.hemant.graphql.dal.ArticleDAL; importcom.hemant.graphql.model.Article; importcom.hemant.graphql.model.Feedback; importcom.hemant.graphql.model.pagination.SortOrder; @Service publicclassArticleServiceImplimplementsArticleService { @Autowired privateArticleDALarticleDAL; privatestaticfinal Logger LOG = LoggerFactory.getLogger(ArticleServiceImpl.class); @Override public List<Article>getAllArticles(intpageNumber, intpageSize, SortOrdersortOrder, String sortBy) { String validationErrors = validatePaginationParams(pageNumber, pageSize, sortOrder, sortBy); if(isNotBlank(validationErrors)) { thrownewIllegalArgumentException(validationErrors); } returnarticleDAL.getAllArticles(pageNumber, pageSize, sortOrder, sortBy); } @Override public Article createArticle(String name, String createdByUserId) { if(isBlank(name)) { thrownewIllegalArgumentException("Article name cannot be blank"); } if(isBlank(createdByUserId)) { thrownewIllegalArgumentException("CreatedByUserId cannot be blank"); } String id = newObjectId().toString(); Article art = new Article(); art.setId(id); art.setCreatedByUserId(createdByUserId); art.setName(name); art.setCreatedOn(new Date()); art.setLastUpdatedOn(new Date()); articleDAL.saveArticle(art); LOG.info("Article created successfully :{}", art); return art; } @Override public List<Feedback>getFeedbacksForArticle(String articleId) { returnarticleDAL.getFeedbacksForArticle(articleId); } @Override public Feedback createFeedback(String feedbackText, String articleId, String createdByUserId) { if(isBlank(feedbackText)) { thrownewIllegalArgumentException("FeedbackText name cannot be blank"); } if(isBlank(createdByUserId)) { thrownewIllegalArgumentException("CreatedByUserId cannot be blank"); } Article article = articleDAL.getArticleById(articleId); if(null == article) { LOG.error("No article exists for articleId :{}", articleId); thrownewIllegalArgumentException("No article exists for articleId :" + articleId); } Feedback feedback = new Feedback(); feedback.setArticleId(articleId); feedback.setCreatedByUserId(createdByUserId); feedback.setFeedbackText(feedbackText); feedback.setCreatedOn(new Date()); feedback.setLastUpdatedOn(new Date()); articleDAL.saveFeedback(feedback); LOG.info("Feedback created as :{} for article :{}", feedback, article); return feedback; } public String validatePaginationParams(intpageNumber, intpageSize, SortOrdersortOrder, String sortBy) { List<String>validationErrors = new ArrayList<>(); if (pageNumber<0) { LOG.error("Minimum PageNumber=0. PageNumber :{} must be greater than 0", pageNumber); validationErrors.add("PageNumber must be greater than or equal to 0!"); } if (pageSize<1) { LOG.error("PageSize :{} must be greater than 0", pageSize); validationErrors.add("PageSize must be greater than 0"); } if (null==sortOrder) { LOG.error("SortOrder :{} must be specified", sortOrder); validationErrors.add("SortOrder must be specified"); } if (StringUtils.isBlank(sortBy)) { LOG.error("SortBy :{} must be specified", sortBy); validationErrors.add("SortBy must be specified"); } if (CollectionUtils.isEmpty(validationErrors)) { returnnull; } returnStringUtils.join(validationErrors, "/n" ); } }

2.3.8 Data Access layer

This layer defines the interaction with database ieMongoDB.

packagecom.hemant.graphql.dal; importjava.util.List; importcom.hemant.graphql.model.Article; importcom.hemant.graphql.model.Feedback; importcom.hemant.graphql.model.pagination.SortOrder; publicinterfaceArticleDAL { List<Article>getAllArticles(intpageNumber, intpageSize, SortOrdersortOrder, String sortBy); voidsaveArticle(Article art); List<Feedback>getFeedbacksForArticle(String articleId); Article getArticleById(String articleId); voidsaveFeedback(Feedback feedback); } packagecom.hemant.graphql.dal; importjava.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.data.domain.Sort; importorg.springframework.data.mongodb.core.MongoTemplate; importorg.springframework.data.mongodb.core.query.Criteria; importorg.springframework.data.mongodb.core.query.Query; importorg.springframework.stereotype.Repository; importcom.hemant.graphql.model.Article; importcom.hemant.graphql.model.Feedback; importcom.hemant.graphql.model.pagination.SortOrder; @Repository publicclassArticleDALImplimplementsArticleDAL { privatestaticfinal Logger LOG = LoggerFactory.getLogger(ArticleDALImpl.class); @Autowired privateMongoTemplate mongo; @Override public List<Article>getAllArticles(intpageNumber, intpageSize, SortOrdersortOrder, String sortBy) { finalint limit = pageSize; finalint offset = pageNumber * pageSize; Query query = new Query(); query.skip(offset).limit(limit).with(new Sort(Sort.Direction.fromString(sortOrder.toString()), sortBy)); LOG.info("The pagination query is limit:{}, offset:{}, sort:{}", query.getLimit(), query.getSkip(), query.getSortObject()); returnmongo.find(query, Article.class); } @Override publicvoidsaveArticle(Article art) { mongo.save(art); } @Override public List<Feedback>getFeedbacksForArticle(String articleId) { Query query = new Query(); query.addCriteria(Criteria.where("articleId").is(articleId)); returnmongo.find(query, Feedback.class); } @Override public Article getArticleById(String articleId) { returnmongo.findById(articleId, Article.class); } @Override publicvoidsaveFeedback(Feedback fb) { mongo.save(fb); } }

2.3.9 Spring Boot Starter class:

The final piece of puzzle is main class for spring boot.

packagecom.hemant.graphql; importorg.springframework.boot.SpringApplication; importorg.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication publicclassSpringBootGraphqlApp { publicstaticvoidmain(String[] args) { SpringApplication.run(SpringBootGraphqlApp.class, args); } }

3. Executing The Project

3.1 Running the application

1. Run the main application class (SpringBootGraphqlApp.java) as Java web development in IDE (Eclipse/IntelliJ) OR

2. If you want to run it from terminal, then

  • Go to root directory, execute “mvn clean install”. This will create the jar in /target child directory.
  • Execute the jar as “java -jar target/spring-boot-graphql-0.0.1-SNAPSHOT.jar

3. Once the application is started, its endpoints can be called.

4. Once application is started, you can go to “ http://localhost:8080/graphiql” in your browser. This will launch graphiql - an electron application to test our graphQL endpoints. This comes with content-assist, auto-complete for building quick building of queries and mutations.

spring

3.2 Sample User Flow

3.2.1 Let’s start by creating a few Articles.

spring

Here we have asked only id, name, createdByUserId and createdOn attributes.

3.2.2 Now lets create 1 more article, now asking for only id, name

spring

3.2.3 Lets list all the articles :

query{ getAllArticles(pageNumber:0, pageSize:5, sortOrder:ASC, sortBy: "name"){ id, name, createdOn } } The response is : { "data": { "getAllArticles": [ { "id": "5b2a4cbb7ae2648fbaa2ce9e", "name": "A1", "createdOn": "Wed Jun 20 18:16:51 IST 2018" }, { "id": "5b2a4d417ae2648fbaa2ce9f", "name": "A2", "createdOn": "Wed Jun 20 18:19:05 IST 2018" } ] } }

As predicted, we only got id, name and createdOn for each article fetched.

3.2.4 Lets create a feedback against an article :

mutation { createNewFeedback(feedbackText: "fB1", articleId : "5b2a4cbb7ae2648fbaa2ce9e", createdByUserId:"1") { id } } Result is : { "data": { "createNewFeedback": { "id": "5b2a4e1d7ae2648fbaa2cea0" } } }

3.2.5 List Feedbacks :

After creating a few more feedbacks, lets list the feedbacks for an article

query{ getFeedBacksForArticle(articleId:"5b2a4cbb7ae2648fbaa2ce9e") { id feedbackText } } { "data": { "getFeedBacksForArticle": [ { "id": "5b2a4e1d7ae2648fbaa2cea0", "feedbackText": "fB1" }, { "id": "5b2a4e577ae2648fbaa2cea1", "feedbackText": "fB2" }, { "id": "5b2a4e5a7ae2648fbaa2cea2", "feedbackText": "fB2" }, { "id": "5b2a4e5a7ae2648fbaa2cea3", "feedbackText": "fB2" }, { "id": "5b2a4e5a7ae2648fbaa2cea4", "feedbackText": "fB2" }, { "id": "5b2a4e5b7ae2648fbaa2cea5", "feedbackText": "fB2" }, { "id": "5b2a4e5b7ae2648fbaa2cea6", "feedbackText": "fB2" }, { "id": "5b2a4e5b7ae2648fbaa2cea7", "feedbackText": "fB2" }, { "id": "5b2a4e5b7ae2648fbaa2cea8", "feedbackText": "fB2" }, { "id": "5b2a4e5b7ae2648fbaa2cea9", "feedbackText": "fB2" } ] } }

Conclusion

Thus we have tested the graphQL application and its end-points using graphIQL interface.

We have queried the graphQL schema, using query and verified that it returns only those attributes which client explicitly asks in API.

We also performed server changes, using mutations. Again we verified that it returns only those attributes which client explicitly asks in API.

This is quite a different API building experience than typical REST APIs.

Software Development Team
Need Software Development Team?
captcha
🙌

Thank you!
We will contact soon.

Oops! Something went wrong.

Recent Blogs

Categories

NSS Note
Trusted by Global Clients