JavaScript Required

We're sorry, but we doesn't work properly without JavaScript enabled.

Looking for an Expert Development Team? Take two weeks 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 :

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

Conclusion

Recent Blogs

Categories

NSS Note

Some of our clients

team