GraphQL + Spring Boot example

GraphQL is a query language for APIs that was developed by Facebook in 2012 and open-sourced in 2015. It provides a powerful, flexible alternative to REST by allowing clients to request exactly the data they need in a single request, thereby reducing over-fetching and under-fetching issues. While GraphQL offers great advantages such as efficient data retrieval and strong typing, it also comes with challenges like increased complexity in caching and potential performance pitfalls if queries are not well optimized.

1. Introduction to GraphQL

What is GraphQL?

GraphQL is a declarative data fetching language that lets clients specify exactly what data they need. Unlike REST, where the structure of the response is defined by the server, GraphQL responses mirror the shape of the query. This flexibility allows for:

  • Efficient Data Retrieval: Clients can request only the necessary fields.
  • Strong Typing: A predefined schema ensures type safety.
  • Single Endpoint: Unlike multiple REST endpoints, GraphQL uses one endpoint for all data queries.

Pros and Cons

Pros:

  • Flexible and Efficient: Clients fetch exactly what they need.
  • Single Request: Reduces the number of network calls.
  • Strongly Typed Schema: Provides clear documentation and validation.

Cons:

  • Complexity: Learning curve can be steep, especially for caching and performance tuning.
  • Overhead: May require additional tooling and infrastructure for schema management.
  • Security: Requires careful query complexity analysis to prevent abuse.

2. Basic GraphQL Syntax in Spring Boot

In Spring Boot applications, GraphQL is typically integrated using the spring-boot-starter-graphql dependency. The core components include:

  • Schema File (schema.graphqls): Defines the types, queries, and mutations.
  • Query Resolvers: Methods annotated with @QueryMapping that resolve queries.
  • Mutation Resolvers: Methods annotated with @MutationMapping that handle data modifications.

For example, a simple schema might define a Device and Owner type, along with queries to fetch them and mutations to create new ones. In our project, this is reflected in the code files such as:

  • QueryResolver.class – Contains query methods like devices(), deviceById(Long id), owners(), and ownerById(Long id).
  • MutationResolver.class – Provides mutation methods to create an owner and a device.

Final project structure:

3. Project Setup

Both the producer and consumer parts of our GraphQL project are created using Spring Initializr. For this example, we include the following dependencies:

  • Spring Boot Starter GraphQL for GraphQL support.
  • Spring Boot Starter Data JPA for persistence.
  • H2 Database for an in-memory database.

The final pom.xml for the project is similar to the following, with all necessary dependencies added:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>tech.devblueprint</groupId>
    <artifactId>graphql-spring-boot-example</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>graphql-spring-boot-example</name>
    <description>Demo project for Spring Boot</description>
    <url/>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-graphql</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.graphql</groupId>
            <artifactId>spring-graphql-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

Additionally, the application.properties file is configured to use the H2 in-memory database and to set the GraphQL endpoint (default is /graphql).

spring.application.name=graphql-spring-boot-example
# H2 Database configuration
spring.datasource.url=jdbc:h2:mem:deviceownerdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# JPA / Hibernate configuration
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update

# Enable H2 console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# GraphQL endpoint (default is /graphql)
spring.graphql.path=/graphql

4. Project Implementation

Entity and Repository Layers

Our domain model consists of two entities:

Device – Represents a device and includes fields like id and name, and a reference to its owner.

package tech.devblueprint.graphql_spring_boot_example.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Device {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  // Many devices can belong to one owner
  @ManyToOne
  @JoinColumn(name = "owner_id")
  private Owner owner;
}

Owner – Represents the owner of one or more devices and includes fields like id and name.

package tech.devblueprint.graphql_spring_boot_example.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Owner {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  // One owner can have many devices
  @OneToMany(mappedBy = "owner", cascade = CascadeType.ALL)
  private List<Device> devices;
}

Repositories are straightforward Spring Data JPA interfaces:

package tech.devblueprint.graphql_spring_boot_example.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import tech.devblueprint.graphql_spring_boot_example.entity.Device;

@Repository
public interface DeviceRepository extends JpaRepository<Device, Long> {
}
package tech.devblueprint.graphql_spring_boot_example.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import tech.devblueprint.graphql_spring_boot_example.entity.Owner;

@Repository
public interface OwnerRepository extends JpaRepository<Owner, Long> {
}

GraphQL Schema (schema.graphqls)

The schema file defines the structure of your API and serves as a contract between the client and server. Key elements include:

Full schema: 

# GraphQL schema definition for Device and Owner

type Device {
  id: ID!
  name: String!
  owner: Owner!
}

type Owner {
  id: ID!
  name: String!
  devices: [Device!]!
}

type Query {
  # Returns all devices
  devices: [Device!]!
  # Returns a device by its id
  deviceById(id: ID!): Device
  # Returns all owners
  owners: [Owner!]!
  # Returns an owner by id
  ownerById(id: ID!): Owner
}

type Mutation {
  # Creates a new owner
  createOwner(name: String!): Owner
  # Creates a new device for an existing owner
  createDevice(name: String!, ownerId: ID!): Device
}

Explanation: 

Type Definitions:
Here, you define the data models (e.g., Device and Owner). Each type includes fields with their respective data types. For example:

type Device {
    id: ID!
    name: String!
    owner: Owner
}

type Owner {
    id: ID!
    name: String!
    devices: [Device]
}

In this snippet:

  • The ! indicates that the field is non-nullable.
  • The [Device] denotes a list of Device objects.

Query Type:
The Query type defines the entry points for read operations. For instance, you might have queries to fetch all devices or a single device by its ID:

type Query {
    devices: [Device]
    deviceById(id: ID!): Device
    owners: [Owner]
    ownerById(id: ID!): Owner
}

Mutation Type:
The Mutation type specifies write operations. In our project, we have mutations for creating owners and devices:

type Mutation {
    createOwner(name: String!): Owner
    createDevice(name: String!, ownerId: ID!): Device
}

 

GraphQL Resolvers

The resolver classes are responsible for mapping the operations defined in the GraphQL schema to actual Java methods. In our project, we have two key resolver classes:

MutationResolver

Purpose:
Handles mutations defined in the schema, allowing clients to create or modify data.

package tech.devblueprint.graphql_spring_boot_example.resolver;

import tech.devblueprint.graphql_spring_boot_example.entity.Device;
import tech.devblueprint.graphql_spring_boot_example.entity.Owner;
import tech.devblueprint.graphql_spring_boot_example.repository.DeviceRepository;
import tech.devblueprint.graphql_spring_boot_example.repository.OwnerRepository;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.stereotype.Controller;

@Controller
public class MutationResolver {

    private final DeviceRepository deviceRepository;
    private final OwnerRepository ownerRepository;

    public MutationResolver(DeviceRepository deviceRepository, OwnerRepository ownerRepository) {
        this.deviceRepository = deviceRepository;
        this.ownerRepository = ownerRepository;
    }

    @MutationMapping
    public Owner createOwner(@Argument String name) {
        Owner owner = new Owner();
        owner.setName(name);
        return ownerRepository.save(owner);
    }

    @MutationMapping
    public Device createDevice(@Argument String name, @Argument Long ownerId) {
        Owner owner = ownerRepository.findById(ownerId).orElse(null);
        if (owner == null) {
            throw new RuntimeException("Owner not found with id " + ownerId);
        }
        Device device = new Device();
        device.setName(name);
        device.setOwner(owner);
        return deviceRepository.save(device);
    }
}

Key Methods:

  • createOwner(@Argument String name):
    This method corresponds to the createOwner mutation. It receives the owner’s name as an argument, creates a new Owner entity, and saves it to the database.

  • createDevice(@Argument String name, @Argument Long ownerId):
    This method maps to the createDevice mutation. It takes the device’s name and the owner ID, fetches the corresponding owner, creates a new Device entity linked to that owner, and then persists it.

GraphQL Syntax Involved:

  • @MutationMapping: Annotation that indicates the method should handle a GraphQL mutation.
  • @Argument: Binds a GraphQL argument to the method parameter.

These annotations allow Spring to automatically route mutation requests from GraphQL to these methods.

QueryResolver

Purpose:
Handles queries, which are used to fetch data from the application.

package tech.devblueprint.graphql_spring_boot_example.resolver;

import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
import tech.devblueprint.graphql_spring_boot_example.entity.Device;
import tech.devblueprint.graphql_spring_boot_example.entity.Owner;
import tech.devblueprint.graphql_spring_boot_example.repository.DeviceRepository;
import tech.devblueprint.graphql_spring_boot_example.repository.OwnerRepository;

import java.util.List;

@Controller
public class QueryResolver {

    private final DeviceRepository deviceRepository;
    private final OwnerRepository ownerRepository;

    public QueryResolver(DeviceRepository deviceRepository, OwnerRepository ownerRepository) {
        this.deviceRepository = deviceRepository;
        this.ownerRepository = ownerRepository;
    }

    @QueryMapping
    public List<Device> devices() {
        return deviceRepository.findAll();
    }

    @QueryMapping
    public Device deviceById(@Argument Long id) {
        return deviceRepository.findById(id).orElse(null);
    }

    @QueryMapping
    public List<Owner> owners() {
        return ownerRepository.findAll();
    }

    @QueryMapping
    public Owner ownerById(@Argument Long id) {
        return ownerRepository.findById(id).orElse(null);
    }
}

Key Methods:

  • devices(): Resolves the devices query by fetching and returning a list of all Device entities.

  • deviceById(@Argument Long id): Handles the deviceById query, which retrieves a single device based on its ID.

  • owners() and ownerById(@Argument Long id): These methods are similar to the device queries but work with Owner entities instead.

GraphQL Syntax Involved:

  • @QueryMapping: Annotation that designates a method as the resolver for a GraphQL query.
  • @Argument: Used to pass query parameters to the resolver method.

5. Testing GraphQL Queries

After setting up the project, we perform several text-based GraphQL queries to test the API:

Conclusion

In this article, we introduced GraphQL, discussed its origins, advantages, and drawbacks, and outlined its core syntax as used in Spring Boot applications. We then detailed how to create a GraphQL project using Spring Initializr, covering entity creation, repository setup, and the implementation of query and mutation resolvers. Finally, we demonstrated how to perform textual queries for creating and retrieving entities.