Effortless Data Fetching: Spring Boot Meets GraphQL

Effortless Data Fetching: Spring Boot Meets GraphQL

Seamless Data Fetching with Spring Boot and GraphQL: A Step-by-Step Tutorial

Introduction

GraphQL has evolved as a strong query language and runtime in the area of contemporary API development, allowing developers to rapidly collect data from diverse sources. This blog article will serve as your beginner guide to dive into GraphQL, whether you're a backend developer, a frontend engineer, or simply inquisitive about the current developments in web development.

GraphQL is an open-source technology developed by Facebook that provides a flexible and fast method for data retrieval and processing. It provides a declarative vocabulary for expressing data needs as well as a runtime for resolving those requirements into a single answer. GraphQL has been widely embraced by organizations such as GitHub, Shopify, and Yelp because of its expanding popularity.

GraphQL is an alternative to REST, SOAP, or gRPC.

Pre-requisites

Bootstrap Spring Boot Application

We will bootstrap our Spring Boot Application with the required dependency as below via Spring IO Initialzr.

Dependencies

  • Spring Web

  • Spring for GraphQL

  • Spring Boot Actuator (Optional)

  • Spring Boot DevTools (Optional)

Just unzip the Generated project and import the maven project in the IDE of your choice.

We will just expose a basic Health Controller to verify if the application is fine and has no issues.

HealthController.java

package in.virendraoswal;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HealthController {
    @GetMapping(path = "/health")
    public String health() {
        return "OK";
    }
}

We will also define two models named Employee and Department which will act as Data for our application.

Employee.java

package in.virendraoswal.model;

public record Employee(String id, String name, int salary, String departmentId) {
}

Department.java

package in.virendraoswal.model;

public record Department(String id, String name, String location) {
}

The relation between Employee and Department is 1-2-1 mapping, where an Employee works as a part of one department.

Setting up Data

Data may be obtained from anywhere, which is a significant strength of GraphQL. Data can be retrieved from a database, an external service, or a static list in memory.

In case of demo purposes, we will be setting up Static List in Memory for both Employee and Department data.

We will define 2 repositories simulating external sources as below

EmployeeRepository.java

package in.virendraoswal.repository;

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

import org.springframework.stereotype.Repository;

import in.virendraoswal.model.Employee;
import in.virendraoswal.model.EmployeeInput;
import jakarta.annotation.PostConstruct;

@Repository
public class EmployeeRepository {
    private List<Employee> employeeData;

    public Employee getById(String id) {
        return employeeData.stream().filter(book -> book.id().equals(id)).findFirst().orElse(null);
    }

    public List<Employee> employees() {
        return employeeData;
    }

    public Employee addEmployee(EmployeeInput employee) {
        String id = String.valueOf(System.currentTimeMillis());
        Employee newEmp = new Employee(id, employee.name(), employee.salary(), employee.departmentId());
        employeeData.add(newEmp);
        return newEmp;
    }

    @PostConstruct
    public void setupData() {
        employeeData = new ArrayList<>();
        employeeData.add(new Employee("e-1", "Joshua", 23000, "d-1"));
        employeeData.add(new Employee("e-2", "Karen", 15000, "d-2"));
        employeeData.add(new Employee("e-3", "Mitchell", 19000, "d-3"));
    }
}

DepartmentRepository.java

package in.virendraoswal.repository;

import java.util.Arrays;
import java.util.List;

import in.virendraoswal.model.Department;

public class DepartmentRepository {
    private static List<Department> departments = Arrays.asList(new Department("d-1", "HR", "Sydney"),
            new Department("d-2", "Finance", "Paris"), new Department("d-3", "Legal", "Mumbai"));

    public static Department getById(String id) {
        return departments.stream().filter(Department -> Department.id().equals(id)).findFirst().orElse(null);
    }
}

We will above 2 repositories to perform CRUD operations on our employee and department data.

GraphQL Implementation

@QueryMapping, @MutationMapping, and @SubscriptionMapping are meta-annotations with the typeName set to Query, Mutation, or Subscription. These are effectively shortcut annotations for fields of the Query, Mutation, and Subscription types, respectively.

@QueryMapping

@QueryMapping is a composed annotation that acts as a shortcut for @SchemaMapping with typeName="Query". Its basic functionality is to retrieve or request data from the GraphQL server with or without @Argument

To use Query Mapping, we will first define a query in our GraphQL Schema files which hosts all queries, types, mutations, etc.

Create a file named schema.graphqls under src/main/resources/graphql

Load Single Employee and Department

To load single employees, we will define below query in our graphql schema file as below along with an argument

type Query {
    employeeById(id: ID): Employee
}

type Employee {
    id: ID
    name: String
    salary: Int
    department: Department
}

type Department {
    id: ID
    name: String
    location: String
}

Here we define employees as a query and the return type will be a list of Employee type/object. Types are defined too as shown above which can be queried as part of data retrieval.

Now we will define @QueryMapping as below in our @Controller class.

@Controller
public class EmployeeController {

    @Autowired
    EmployeeRepository employeeRepository;

    @QueryMapping
    public Employee employeeById(@Argument String id) {
        return employeeRepository.getById(id);
    }

    @SchemaMapping
    public Department department(Employee employee) {
        return DepartmentRepository.getById(employee.departmentId());
    }
}

By defining a method named employeeById annotated with @QueryMapping, this controller declares how to fetch an Employee as defined under the Query type. The query field is derived from the method name, but can also be declared on the annotation itself.

DataFetchingEnvironment in the GraphQL Java engine offers access to a map of field-specific argument values. To have an argument tied to a target object and injected into the controller method, use the @Argument annotation. The method parameter name is used by default to look for the argument, but it may also be supplied on the annotation itself.

This employeeById function defines how to obtain a single Employee but does not handle retrieving the associated Department. If the request specifies the department, GraphQL Java will need to get this data.

The @SchemaMapping annotation associates a handler function with a GraphQL schema field and defines it to be the DataFetcher for that field. The method name is the default field name, and the type name is the basic class name of the source/parent object injected into the method. In this case, the field is set to department and the type is set to Employee.

Enable GraphQL playground

GraphiQL is a useful visual interface for query development and execution, among other things. Add the below configuration to the application.properties to enable GraphiQL

spring.graphql.graphiql.enabled=true

Start the application, and visit http://localhost:8080/graphiql and fire below query

query employeeDetails {
  employeeById(id: "e-1") {
    id
    name
    salary
    department {
      id
      name
      location
    }
  }
}

On executing a query, we will get the result below. When executing, you can specify what all data the client need which is the most impressive thing about it.

Congratulations on creating a GraphQL service and running the first query! We were able to accomplish this with only a few lines of code thanks to Spring for GraphQL.

Load All Employees

GraphQL query needs to be updated as below, we need to return the List of Employees using []

type Query {
    employeeById(id: ID): Employee
    employees: [Employee]
}

Add the below method to invoke the GraphQL data fetcher. We can directly use QueryMapping but just want to show how to invoke a query using Schema Mapping the result would be as below as its Schema Mapping is a superset of all annotations.

    @SchemaMapping(typeName = "Query")
    public List<Employee> employees() {
        return employeeRepository.employees();
    }

Now fire Query using GraphiQL playground, and voila!

Let's say as a client we want to fetch only the Name of the Employee and its Department name along with their IDs, we can update the Query as below without changing the server contract or adding a new API as below

This is what is called Efficient Data Fetching.

Efficient Data Fetching: GraphQL allows clients to request only the specific data they need, reducing over-fetching and under-fetching of data. This leads to faster and more efficient data retrieval, especially on mobile or low-bandwidth networks.

@MutationMapping

A mutation mapping is used essentially for a change to our data, such as adding, updating, or deleting records.

Mutations are mapped the same way as Query, we will add one mutation named addEmployee to add an employee to our In-Memory Data. The concept in terms of mapping method to Mutation is the same.

Add Mutation to schema.graphqls as below

type Mutation {
  addEmployee(employee: EmployeeInput!): Employee
}

Here we will be passing EmployeeInput as a complex object which is similar to passing an object in the form of RequestBody in REST.

! post EmployeeInput above denotes it's a required field.

    @MutationMapping
    public Employee addEmployee(@Argument EmployeeInput employee) {
        return employeeRepository.addEmployee(employee);
    }

We just have simulated adding of Employee to the data source.

Entire graphql/schema.graphqls looks like below

type Query {
    employeeById(id: ID): Employee
    employees: [Employee]
}

type Mutation {
  addEmployee(employee: EmployeeInput!): Employee
}

type Employee {
    id: ID
    name: String
    salary: Int
    department: Department
}

type Department {
    id: ID
    name: String
    location: String
}

input EmployeeInput {
       name: String
    salary: Int
    departmentId: String
}

We will now fire Mutation from our GraphiQL playground as below

Voila! We have added mutation to our GraphQL service.

Advantages of using GraphQL

  • Efficient Data Fetching

  • Flexible and Declarative Queries

  • Versioning and Evolution

  • Aggregation and Composition

There are many other advantages too, but the above 4 are one of the important features which make API development easier.

Resources

NOTE: This blog article will be updated/edited to add more learnings.


Thank you for reading, If you have reached it so far, please like the article, It will encourage me to write more such articles. Do share your valuable suggestions, I appreciate your honest feedback and suggestions!

I would love to connect with you on Twitter | LinkedIn

Did you find this article valuable?

Support Virendra Oswal's Blog by becoming a sponsor. Any amount is appreciated!