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 :



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


1.1 RESTful API

1.2 Problems with Restful API:

1.3 GraphQL Server

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

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 { 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



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 /><!-- lookup parent from repository --> </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 :


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


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" } ] } }

4. Conclusion

  • img
  • img
  • img
  • img
  • img
  • img
  • img
  • img
  • img
  • img
  • img
  • img
  • img
  • img
  • img
  • img
  • img