GraphQL is one of the hot choices for developers and its popularity is increasing day by day. With its increasing demand and popularity among front end developers, we think it may fully replace RESTful APIs in the future. Developed by Facebook, GraphQL claims to be the "better REST" and solves the problem of over-fetching and under-fetching that is prevalent with REST APIs.

GraphQL is query friendly and allows you to define the structure and format of the data you want to fetch. It also allows you to query multiple models in a single request. Here is an example

Image Source: https://www.howtographql.com/basics/1-graphql-is-the-better-rest/ 

A single POST request was not only able to fetch data from 3 different models, i.e. user, posts, and followers but also format it as per the client's needs. On the other hand, it would have us taken 3 requests with a RESTful service to get the same data, plus it would need additional formatting and mapping to make it consumable by the client. GraphQL can help us save us several seconds of processing time and also shortens the response payload to a bare minimum.

What is a CRUD API?

CRUD stands for create, read, update, delete. They are the most basic operations that an API can offer. The client application should be able to seamlessly perform CRUD operations on the model. The faster these operations are, the slicker your client application will feel. Almost every web application needs to handle thousands of CRUD requests per minute. What happens when you do a Facebook post? A post object is created by calling the Facebook API and it is stored in the database. Every time you go to your timeline, the same post is fetched on the UI to display. When you update the post, an update operation takes place and the contents of the post are updated in the database. The user also has the option to delete a post and it is purged from the database. CRUD is the basic building block of every massive API or application that we use.

What will we create?

We are going to create a simple CRUD API using GraphQL to manage posts. Let's say we are talking about posts that are present on a blog site like this one. We will build the API that serves as the backend of the blog and lets you perform the basic CRUD operations needed by a client application. We will be using MongoDB as the database to store the posts. A NoSQL database is the ideal choice for lightweight or mid-sized applications, that have a flat data model structure, and hence we would go for MongoDB in this example.

Getting Started

Like any other spring boot project, we would start by navigating to Spring Initializr. It is a great place to bootstrap your development and avoid the pain of writing a ton of boilerplate code. Enter your project information and select the dependencies "Lombok", "Spring Data MongoDB" and "Spring Web". We will be adding other dependencies later on. Make sure to select Maven and Java 8 for your project.

Use spring initializr to generate a template project

Click generate and download the source code. Open the project in your favorite IDE. We will be using IntelliJ Idea for but you are free to use any Java IDE of your choice.

Add dependencies for GraphQL, JUnits and some other utilities that are required for this project to your pom.xml.

<!-- GraphQL and related dependencies -->
<dependency>
    <groupId>com.graphql-java-kickstart</groupId>
    <artifactId>graphql-spring-boot-starter</artifactId>
    <version>5.7.0</version>
</dependency>
<dependency>
    <groupId>com.graphql-java-kickstart</groupId>
    <artifactId>graphiql-spring-boot-starter</artifactId>
    <version>5.5.0</version>
</dependency>
<dependency>
    <groupId>com.graphql-java-kickstart</groupId>
    <artifactId>graphql-java-tools</artifactId>
    <version>5.4.1</version>
</dependency>

<!-- JUnits for unit testing -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
</dependency>

<!-- JodaTime for using date time audit features in MongoDB -->
<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
    <version>2.10.6</version>
</dependency>
pom.xml

The additional dependencies added to the pom are described below:

  • GraphQL spring boot starter - Core GraphQL Java libraries
  • GraphIQL spring boot starter - For GraphIQL GUI used for sending requests
  • GraphQL Java Tools - GraphQL utilities and error handling
  • JUnit - For writing unit tests to test our GraphQL API
  • Joda Time - Used for date and time fields that utilize MongoDB's audit features

Sync your maven project to pull the dependencies. Make sure to stick to the versions that we have used for the dependencies. Incompatibility between two dependency versions can really screw up your code and give you a hard time. We will now set up the MongoDB database needed for this project.

Setup MongoDB

Make sure you have MongoDB community server up and running on your local. If not, you can download and install it from here. You will also need Mongo DB Compass, which is a GUI application used to connect with your database and perform administration tasks.

Create application.yml file in your main/resources directory and add the following lines:

spring:
 data:
  mongodb:
   database: posts
   host: localhost
   port: 27017
   
graphiql:
 subscriptions:
  timeout: 300000
application.yml

Verify that your MongoDB server is up and running on port 27017 using Compass or by entering "mongo" on your terminal.

Defining the GraphQL schema

The next step is to define our GraphQL schema which consists of queries and mutations. Queries are used to fetch data in GraphQL whereas mutations modify data. Add the below schema in a schema.graphqls file in main/resources directory.

schema {
    query: Query
    mutation: Mutation
}

type Post {
    id: String
    content: String
    likedBy: [User]
    comments: [String]
    createdAt: String
    lastModified: String
    version: Int
}

type User {
    username: String
    name: String
    email: String
}

type Query {
    post(id : String!): Post
    allPosts : [Post]
}

type Mutation {
    createPost(input: PostInput!) : Post
    deletePost(id : String!) : Boolean
    updatePost(input: PostInput!) : Post
    likePost(postId: String!, user: UserInput!) : Post
    addComment(postId: String!, comment: String!) : Post
}

input PostInput {
    id: String
    content: String
}

input UserInput {
    username: String
    name: String
    email: String
}
schema.graphqls

A type in the schema file defines the data structure of an object. We have defined a Post and a User. A post can be liked by multiple users and hence a post will have a list called "likedBy" to store the list of users who like the post. A post can also have a list of comments on it. Note that a Query and Mutation are also defined as types, all operations being defined inside them. Apart from the basic CRUD operations on a post, we have methods to like a post and add a comment to a post. In GraphQL an input to a method cannot be the same as the type, so we need to create separate inputs called "PostInput" and "UserInput" for this purpose, we have only defined the fields that are necessary while taking input from the user in this object.

Writing Java Code

Now it's time to create a Java class to define the Post object. It should match with our graphql schema and have the same fields.

Create Post.java in the main/java/model directory.

package com.techinjektion.graphqlapi.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.joda.time.DateTime;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.annotation.Version;
import org.springframework.data.mongodb.core.mapping.Document;

import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Document("post")
public class Post {
    ObjectId id;
    String content;
    List<User> likedBy;
    List<String> comments;
    @Version
    private Long version;
    @CreatedDate
    private DateTime createdAt;
    @LastModifiedDate
    private DateTime lastModified;
}
Post.java

The @Document annotation is used to specify the name of the document for MongoDB. All other annotations are from lombok and help us reduce boilerplate code like constructors, builders, etc. We also need to define the User class in the model directory. We must have the Lombok plugin installed and annotation processing enabled in IntelliJ for the annotations to work. Check this guide on steps to install it.

package com.techinjektion.graphqlapi.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class User {
    String username;
    String name;
    String email;
}
User.java

We will also need a Java class for the Post input type. Only id and content are the fields required for this class.

package com.techinjektion.graphqlapi.input;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class PostInput {
    ObjectId id;
    String content;
}
PostInput.java

We will now create the MongoDB repository interface in main/java/repository

package com.techinjektion.graphqlapi.repository;

import com.techinjektion.graphqlapi.model.Post;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.repository.MongoRepository;

public interface PostRepository extends MongoRepository<Post, ObjectId> {

}

Creating GraphQL Resolvers

With the repository and POJOs created, now we need to do the wiring in resolver classes for the Query and the Mutation types. In the directory main/java/resolvers, create a class called QueryResolver.java

package com.techinjektion.graphqlapi.resolvers;

import com.coxautodev.graphql.tools.GraphQLQueryResolver;
import com.techinjektion.graphqlapi.error.PostNotFoundException;
import com.techinjektion.graphqlapi.model.Post;
import com.techinjektion.graphqlapi.repository.PostRepository;
import lombok.RequiredArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class QueryResolver implements GraphQLQueryResolver {

    private final PostRepository postRepository;

    public Post post(String id) {
        return postRepository.findById(new ObjectId(id))
                .orElseThrow(() -> new PostNotFoundException("Post not found", id));
    }

    public Iterable<Post> allPosts() {
        return postRepository.findAll();
    }

}
QueryResolver.java

The @Component annotation ensures that the class is initialized at server startup. We have added two methods corresponding to the entries in the Query type of the graphql schema file. PostNotFoundException is thrown if the queried post is not found in the repository, we will define this class later.

We need to define another similar class for resolving the mutations. Let's call it MutationResolver.java.

package com.techinjektion.graphqlapi.resolvers;

import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import com.techinjektion.graphqlapi.error.PostNotFoundException;
import com.techinjektion.graphqlapi.input.PostInput;
import com.techinjektion.graphqlapi.model.Post;
import com.techinjektion.graphqlapi.model.User;
import com.techinjektion.graphqlapi.repository.PostRepository;
import lombok.AllArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.stereotype.Component;

import java.util.ArrayList;

@Component
@AllArgsConstructor
public class MutationResolver implements GraphQLMutationResolver {

    private final PostRepository postRepository;

    public Post createPost(PostInput input) {
        Post post = Post.builder()
                .content(input.getContent())
                .likedBy(new ArrayList<User>())
                .comments(new ArrayList<String>())
                .build();
        return postRepository.save(post);
    }

    public Post updatePost(PostInput input) {
        Post post = postRepository.findById(input.getId())
                .orElseThrow(() -> new PostNotFoundException("Post to update was not found",
                        input.getId().toString()));
        post.setContent(input.getContent());
        return postRepository.save(post);
    }

    public boolean deletePost(String id) {
        postRepository.deleteById(new ObjectId(id));
        return true;
    }

    public Post likePost(String postId, User user) {
        Post post = postRepository.findById(new ObjectId(postId))
                .orElseThrow(() -> new PostNotFoundException("Post to like was not found", postId));
        post.getLikedBy().add(user);
        return postRepository.save(post);
    }

    public Post addComment(String postId, String comment) {
        Post post = postRepository.findById(new ObjectId(postId))
                .orElseThrow(() -> new PostNotFoundException("Post to comment was not found", postId));
        post.getComments().add(comment);
        return postRepository.save(post);
    }

}
MutationResolver.java

We have created methods to perform actions on the post repository as per the methods defined in the schema file earlier.

Error Handling

We have already used PostNotFoundException in the resolver classes and now we need to create this exception class. Create the class in main/java/error directory.

package com.techinjektion.graphqlapi.error;

import graphql.ErrorType;
import graphql.GraphQLError;
import graphql.language.SourceLocation;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class PostNotFoundException extends RuntimeException implements GraphQLError {

    private Map<String, Object> extensions = new HashMap<>();

    public PostNotFoundException(String message, String invalidPostId) {
        super(message);
        extensions.put("invalidPostId", invalidPostId);
    }

    @Override
    public ErrorType getErrorType() {
        return ErrorType.DataFetchingException;
    }

    @Override
    public Map<String, Object> getExtensions() {
        return extensions;
    }

    @Override
    public List<SourceLocation> getLocations() {
        return null;
    }
}
PostNotFoundException.java

Set up GraphQLErrorAdapter and GraphQLAPIErrorHandler classes that will help with error handling.

package com.techinjektion.graphqlapi.error;

import graphql.ErrorType;
import graphql.ExceptionWhileDataFetching;
import graphql.GraphQLError;
import graphql.language.SourceLocation;

import java.util.List;
import java.util.Map;

public class GraphQLErrorAdapter implements GraphQLError {

    private GraphQLError error;

    public GraphQLErrorAdapter(GraphQLError error) {
        this.error = error;
    }

    @Override
    public Map<String, Object> getExtensions() {
        return error.getExtensions();
    }

    @Override
    public List<SourceLocation> getLocations() {
        return error.getLocations();
    }

    @Override
    public ErrorType getErrorType() {
        return error.getErrorType();
    }

    @Override
    public List<Object> getPath() {
        return error.getPath();
    }

    @Override
    public Map<String, Object> toSpecification() {
        return error.toSpecification();
    }

    @Override
    public String getMessage() {
        return (error instanceof ExceptionWhileDataFetching) 
                ? ((ExceptionWhileDataFetching) error).getException().getMessage() : error.getMessage();
    }
}
GraphQLErrorAdapter.java
package com.techinjektion.graphqlapi.error;

import graphql.ExceptionWhileDataFetching;
import graphql.GraphQLError;
import graphql.servlet.GraphQLErrorHandler;
import org.springframework.stereotype.Component;

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

@Component
public class GraphQLAPIErrorHandler implements GraphQLErrorHandler {
    @Override
    public List<GraphQLError> processErrors(List<GraphQLError> errors) {
        List<GraphQLError> clientErrors = errors.stream()
                .filter(this::isClientError)
                .collect(Collectors.toList());

        List<GraphQLError> serverErrors = errors.stream()
                .filter(e -> !isClientError(e))
                .map(GraphQLErrorAdapter::new)
                .collect(Collectors.toList());

        List<GraphQLError> e = new ArrayList<>();
        e.addAll(clientErrors);
        e.addAll(serverErrors);
        return e;
    }

    protected List<GraphQLError> filterGraphQLErrors(List<GraphQLError> errors) {
        return errors.stream()
                .filter(this::isClientError)
                .collect(Collectors.toList());
    }

    protected boolean isClientError(GraphQLError error) {
        return !(error instanceof ExceptionWhileDataFetching || error instanceof Throwable);
    }
}
GraphQLAPIErrorHandler.java

Enable Audit Fields

We need to make another minor change to enable the MongoDB audit fields like created time, updated time, and version. Add the @EnableMongoAuditing annotation to your main Spring Boot starter class.

package com.techinjektion.graphqlapi;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.mongodb.config.EnableMongoAuditing;

@SpringBootApplication
@EnableMongoAuditing
public class GraphqlApiApplication {

	public static void main(String[] args) {
		SpringApplication.run(GraphqlApiApplication.class, args);
	}

}
GraphqlApiApplication.java

We should be all set now and are ready to test our GraphQL API.

Test the API using GraphiQL

Navigate to http://localhost:8080/ in your browser and you should be able to see the GraphiQL interface. Send a request with variables to create a new post.

mutation CreatePost($createInput: PostInput!) {
  createPost(input: $createInput) {
    id
    content
    createdAt
    lastModified
    version
  }
}
CreatePost Request
{
  "createInput": {
    "content": "Hello World! This is my first post on GraphQL"
  }
}
Variables
Create Post request and response

You should get a successful response with all the fields that were sent by us in the CreatePost mutation. Go ahead and add a few more posts so that we can test some queries. Then run the query below to get all posts.

query AllPosts {
  allPosts {
    id
    content
    likedBy {
      username
      email
      name
    }
    comments
    createdAt
    lastModified
    version
  }
}
Get All Posts
Get All Posts request and response

GraphQL returns all posts in a list now. Notice that the liked by and comments fields are empty. We will use the methods in our mutations to set them. Copy the id for the first post and we will like it and add comments to it. Run the mutation below to add a like to the post.

mutation LikePost($postId: String!, $userInput: UserInput!) {
  likePost(postId: $postId, user: $userInput) {
    id
    content
    likedBy {
      username
      email
      name
    }
    comments
    createdAt
    lastModified
    version
  }
}
Like Post Mutation
Like Post mutatation request and response

A user is added to the liked by list and the version is incremented from 0 to 1 meaning that the bean was updated.

Let's add a comment to the post now using the addComment mutation.

mutation AddComment($postId: String!, $comment: String!) {
  addComment(postId: $postId, comment: $comment) {
    id
    content
    likedBy {
      username
      email
      name
    }
    comments
    createdAt
    lastModified
    version
  }
}
Add comment mutation
Add Comment mutation request and response

A comment was also added to the post now. You can add multiple likes and comments to the post and then query all posts again to see them together.

See the multiple likes and comments in the first post. Similarly you can play around with the updatePost mutation to update and existing post or the post query to get a single post by id. Verify that all queries and mutations are working. We can also validate the data in MongoDB by opening Compass and checking the posts collection.

Congratulations on building your CRUD API with GraphQL and MongoDB. There are an endless number of things you can do with GraphQL, feel free to play around on your own.

As usual, you can find the source code for this API on my GitHub.