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
JDK17
Maven 3.5+
IDE of your choice. (We are using Spring Tool Suite)
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!