Building, Containerising, and Deploying GraphQL APIs using Java, SpringBoot, Docker, and Kubernetes
Modern application development involves utilising technologies that facilitate scalability, maintainability, and efficient communication between services.
In this article, we will guide you through building a GraphQL API using Java and Spring Boot, containerisation with Docker, and deployment using Kubernetes.
Before diving into the implementation, let's understand the structure of GraphQL, its advantages over REST APIs, and the concepts of containerization with Docker and container orchestration with Kubernetes.
Understanding GraphQL
The Structure of GraphQL
GraphQL is a query language created by Facebook for APIs.
It offers a more efficient, powerful, and flexible alternative to the conventional REST API, as clients can precisely request the data they need, reducing the possibility of over-fetching or under-fetching data. The API schema is explicitly defined, which allows clients to understand and request the exact shape of the response.
The structure of GraphQL is primarily based on three key concepts: queries
, mutations
, and subscriptions
. These elements allow clients to carry out specific operations, such as retrieving data, modifying data, and receiving real-time updates.
Queries: Queries are used to request data from the server.
Clients can specify the shape of the response, indicating the fields they are interested in. Enabling clients to fetch only the necessary data, eliminating unnecessary data transfer and improving application performance.query { getAllBooks { id title author } }
Mutations: Mutations are used to modify data on the server.
Unlike queries, mutations can change the state of the server.
This includes actions like creating, updating, or deleting data.mutation { addBook(title: "The GraphQL Guide", author: "John Doe") { id title author } }
Subscriptions: Subscriptions enable real-time communication between the server and clients. Clients can subscribe to specific events on the server, and when they occur, the subscribed clients receive updates in real-time.
This is particularly useful for applications requiring live data updates.subscription { newBookAdded { id title author } }
GraphQL Schemas, Types, Fields, and Resolvers
GraphQL Schema: At the core of a GraphQL API is the schema.
The schema defines the types, queries, mutations, and subscriptions available in the API. It serves as the contract between the client and the server, specifying how clients can request and interact with the data.
Example Book Schema:
type Book {
id: ID!
title: String!
author: String!
}
type Query {
getAllBooks: [Book]!
getBookById(id: ID!): Book
}
type Mutation {
addBook(title: String!, author: String!): Book!
}
type Subscription {
newBookAdded: Book
}
Types: In GraphQL, types define the shape of the data. Scalars represent primitive data types (e.g., String, Int, ID), and custom types represent more complex structures (e.g., Book in the example schema).
Scalar Types:
ID
: A unique identifier.String
: A sequence of characters.Int
: Integer value.
Custom Type:
Book
: Represents a book with fieldsid
,title
, andauthor
.
Fields: Fields are the individual units of data within a type. Each field has a name and a type, and it represents a piece of information that can be requested in a query.
Book Fields:
id
: Represents the unique identifier of the book.title
: Represents the title of the book.author
: Represents the author of the book.
Query Fields:
getAllBooks
: Represents a query to retrieve a list of all books.getBookById
: Represents a query to retrieve a specific book by its ID.
Mutation Fields:
addBook
: Represents a mutation to add a new book. It takestitle
andauthor
as arguments and returns the added book.
Subscription Fields:
newBookAdded
: Represents a subscription to be notified when a new book is added.
Resolvers: Resolvers are functions responsible for fetching the data for a specific field. In a GraphQL server, each field in the schema has an associated resolver that determines how to retrieve the data.
Book Resolvers:
getAllBooks
: A resolver function that fetches and returns a list of all books.getBookById
: A resolver function that fetches and returns a specific book by its ID.addBook
: A resolver function that adds a new book to the database and returns the added book.newBookAdded
: A resolver function that pushes new book data to subscribed clients when a new book is added.
GraphQL vs REST: Method Comparison
As REST is the most popularly adopted API protocol, this comparison is aimed at helping readers understand the differences between REST APIs and GraphQL from their already established perspective.
Let's compare commonly used HTTP methods in REST APIs with corresponding concepts in GraphQL to highlight the distinctive characteristics of each approach.
1. GET (Retrieving Data):
REST:
Endpoint:
GET /api/books
Action: Retrieves a list of all books.
GraphQL:
Query:
query { getAllBooks { id, title, author } }
Action: Retrieves all books with specific fields (id, title, author).
2. POST (Creating Data):
REST:
Endpoint:
POST /api/books
Action: Creates a new book with data in the request body.
GraphQL:
Mutation:
mutation { addBook(title: "New Book", author: "Author") { id, title, author } }
Action: Creates a new book and returns specified fields (id, title, author).
3. PUT/PATCH (Updating Data):
REST:
Endpoint:
PUT /api/books/{id}
orPATCH /api/books/{id}
Action: Updates the details of a specific book (full or partial update).
GraphQL:
Mutation:
mutation { updateBook(id: 123, title: "Updated Title") { id, title, author } }
Action: Updates specific fields (id, title, author) of a book and returns the updated values.
4. DELETE (Deleting Data):
REST:
Endpoint:
DELETE /api/books/{id}
Action: Deletes a specific book.
GraphQL:
Mutation:
mutation { deleteBook(id: 123) { id, title, author } }
Action: Deletes a specific book and returns the details of the deleted book.
5. Subscription (Real-time Updates):
REST:
- Not natively supported; typically requires additional technologies (e.g., WebSockets).
GraphQL:
Subscription:
subscription { newBookAdded { id, title, author } }
Action: Allows clients to receive real-time updates when a new book is added.
Key Differences:
REST:
Uses different HTTP methods for different operations.
Each endpoint corresponds to a specific resource and operation.
GraphQL:
Utilises a single HTTP method (
POST
) for all operations.Operations (queries, mutations, subscriptions) are specified in the request body.
Advantages of GraphQL over RESTful APIs
Reduced Over-fetching and Under-fetching: Clients can request only the data they need, preventing over-fetching unnecessary information or under-fetching of critical data.
Single Request, Multiple Resources: Clients can request multiple resources in a single query, reducing the number of HTTP requests and improving performance.
Strongly Typed: GraphQL schemas are strongly typed, clarifying the expected data types. This reduces the likelihood of runtime errors.
Introspection: GraphQL APIs support introspection, allowing clients to query the schema itself, which makes it easier for developers to explore and understand the available data and operations.
The choice between GraphQL and REST often revolves around factors like data complexity, real-time requirements, and the need for a flexible query language. GraphQL's single endpoint design and precise data retrieval stand in contrast to REST's convention-driven approach using various HTTP methods and multiple endpoints.
Containerization with Docker
Docker Overview
Docker is a platform that allows developers to create, distribute, and run applications using containers. These containers contain all the necessary dependencies for an application to function, ensuring it can be deployed consistently and reproducibly across various environments. Docker's containers are lightweight, portable, and offer isolation, enabling developers to bundle an application with its dependencies into a single unit.
Container Orchestration with Kubernetes
Kubernetes Overview
Kubernetes is an open-source platform for container orchestration that automates the deployment, scaling, and management of containerised applications.
It abstracts away the underlying infrastructure and provides a unified API for managing clusters of containers. Kubernetes is a popular choice for deploying and managing containerised workloads, as it enables high availability, scalability, and self-healing for applications.
Now that we have a deeper understanding of GraphQL's structure and advantages let's proceed with building a GraphQL API using Java and Spring Boot, containerizing it with Docker, and deploying it to Kubernetes.
API Development and Deployment
Prerequisites
Java Development Kit (JDK): Install JDK for server-side Java development.
You can download and install the latest version of JDK from the official Oracle website. Verify the installation withjava -version
.Docker: Follow the installation instructions for your operating system on the Docker website. After installation, verify Docker by checking
docker --version
.Kubernetes (Minikube): For orchestration, we'll use Kubernetes and Minikube, to be precise, as we will deploy locally. Visit the Minikube Start page and follow the instructions for your host environment to install Minikube. Run the command below in your terminal to verify the installation:
minikube version
Development
Step 1: Set Up a Spring Boot Project
Create a new Spring Boot project using Spring Initializr or your preferred IDE.
Include the Spring Web
dependency for building web applications, Spring Data JPA
dependency for seamless data access, H2 Database
dependency for a lightweight in-memory database and Lombok
to reduce common boilerplate code. Additionally, add the graphql-java
dependency for GraphQL support.
Below are the Maven and Gradle configurations for the dependencies required
Copy and paste the respective configuration into your Maven pom.xml
or Gradle build.gradle
file, depending on your build tool preference. After making the changes, remember to refresh or rebuild your project to apply the new dependencies.
Maven
<!-- 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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>provided</scope>
</dependency>
Gradle
// Dependencies
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-graphql'
implementation 'org.springframework.boot:spring-boot-starter-web'
runtimeOnly 'com.h2database:h2'
compileOnly 'org.projectlombok:lombok:1.18.22'
annotationProcessor 'org.projectlombok:lombok:1.18.22'
application.properties
(Application Configuration)
The application.properties
file configures various settings for the Spring Boot application, including database settings, GraphQL Playground, and server port.
# DataSource Configuration
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
# H2 Console Configuration (Optional, for development purposes)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# JPA Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# GraphQL Playground Configuration (Optional, for development purposes)
spring.graphql.graphiql.enabled=true
# Server Port
server.port=8080
Step 2: Project Structure
Your project structure should look like this:
src
│
└── main
├── java
│ └── com
│ └── practice
│ └── graphqldemo
│ ├── model
│ │ └── Book.java
│ │ └── BookInput.java
│ ├── repository
│ │ └── BookRepository.java
│ ├── service
│ │ └── BookService.java
│ ├── controller
│ │ └── BookController.java
│ └── GraphQLDemoApplication.java
└── resources
├── application.properties
└── graphql
└── schema.graphqls
Step 3: Define a GraphQL Schema
Create a GraphQL schema using the GraphQL Schema Definition Language (SDL)
.
Define the types, queries, and mutations based on your application requirements.
This schema.graphqls
file defines the GraphQL schema, specifying the types (e.g., Book
), queries (e.g., getAllBooks
, getBookById
), and mutations (e.g., addBook
, updateBook
, deleteBook
).
Place this file under src/main/resources/graphql/schema.graphqls
:
type Book {
id: ID!
title: String!
description: String!
}
type Query {
getAllBooks: [Book]!
getBookById(id: ID!): Book
}
input BookInput {
title: String!
description: String!
}
type Mutation {
addBook(bookInput: BookInput): Book
updateBook(id: ID!, bookInput: BookInput): Book
deleteBook(id: ID!): Book
}
Step 4: Code Implementation
Book.java
(Model)
The Book
class represents the entity model for a book. It is annotated with @Entity
to indicate that it is a JPA entity, and will be mapped to a database table.
The @Id
annotation denotes the primary key and @GeneratedValue
specifies that the ID will be automatically generated.
package com.practice.graphqldemo.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Setter
@Getter
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String description;
}
BookInput.java
(Data Transfer Object)
The BookInput
class represents the input data structure for creating or updating a book in the GraphQL API. This class is designed to encapsulate the data needed when adding a new book or modifying an existing one through GraphQL mutations.
package com.practice.graphqldemo.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
@AllArgsConstructor
public class BookInput {
private String title;
private String description;
}
BookRepository.java
(Repository)
The BookRepository
interface extends JpaRepository
to inherit common methods for working with the Book
entity. Spring Data JPA provides powerful, generic CRUD operations, reducing the boilerplate code required for database interactions.
package com.practice.graphqldemo.repository;
import com.practice.graphqldemo.model.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
}
BookService.java
(Service)
The BookService
class contains business logic related to books. It interacts with the BookRepository
to perform CRUD operations on the book resource.
Methods like getAllBooks
, getBookById
, addBook
, updateBook
, and deleteBook
encapsulate their corresponding operations.
package com.practice.graphqldemo.service;
import com.practice.graphqldemo.model.Book;
import com.practice.graphqldemo.model.BookInput;
import com.practice.graphqldemo.repository.BookRepository;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class BookService {
@Autowired
private BookRepository bookRepository;
public List<Book> getAllBooks() {
return bookRepository.findAll();
}
public Optional<Book> getBookById(Long id) {
return bookRepository.findById(id);
}
public Book addBook(BookInput bookInput) {
Book newBook = new Book();
newBook.setTitle(bookInput.getTitle());
newBook.setDescription(bookInput.getDescription());
return bookRepository.save(newBook);
}
public Book updateBook(Long id, BookInput bookInput) {
Optional<Book> optionalBook = bookRepository.findById(id);
if (optionalBook.isPresent()) {
Book book = optionalBook.get();
book.setTitle(bookInput.getTitle());
book.setDescription(bookInput.getDescription());
return bookRepository.save(book);
}
return null;
}
public Book deleteBook(Long id) {
Optional<Book> optionalBook = bookRepository.findById(id);
if (optionalBook.isPresent()) {
Book bookToDelete = optionalBook.get();
bookRepository.deleteById(id);
return bookToDelete;
}
return null;
}
}
BookController.java
(Book Controller)
The BookController
class handles HTTP requests for GraphQL queries and mutations.
package com.practice.graphqldemo.controller;
import com.practice.graphqldemo.model.Book;
import com.practice.graphqldemo.model.BookInput;
import com.practice.graphqldemo.service.BookServiceImplementation;
import lombok.RequiredArgsConstructor;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
import java.util.Optional;
@Controller
@RequiredArgsConstructor
public class BookController {
private final BookServiceImplementation bookServiceImplementation;
@QueryMapping
Iterable<Book> getAllBooks() {
return bookServiceImplementation.getAllBooks();
}
@QueryMapping
Optional<Book> getBookById(@Argument Long id) {
return bookServiceImplementation.getBookById(id);
}
@MutationMapping
Book addBook(@Argument BookInput bookInput) {
return bookServiceImplementation.addBook(bookInput);
}
@MutationMapping
public Book updateBook(@Argument Long id, @Argument BookInput bookInput) {
return bookServiceImplementation.updateBook(id, bookInput);
}
@MutationMapping
public Book deleteBook(@Argument Long id) {
return bookServiceImplementation.deleteBook(id);
}
}
GraphQLDemoApplication.java
(Spring Boot Application)
The GraphQLDemoApplication
class serves as the entry point for the Spring Boot application. It includes the main
method that starts the Spring Boot application.
package com.practice.graphqldemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GraphqlDemoApplication {
public static void main(String[] args) {
SpringApplication.run(GraphqlDemoApplication.class, args);
}
}
Project Build
Here are the steps to build a JAR file for your Spring Boot application using either Maven or Gradle depending on your build tool preference:
Using Maven:
Open a terminal and navigate to your project directory.
Run the following Maven command to build the JAR file:
./mvnw clean package
This command will clean the project, compile the code, run tests, and package the application into a JAR file. The JAR file will be located in the
target
directory.
Using Gradle:
Open a terminal and navigate to your project directory.
Run the following Gradle command to build the JAR file:
./gradlew clean build
This command will clean the project, compile the code, run tests, and package the application into a JAR file. The JAR file will be located in the
build/libs
directory.
Containerization with Docker
Dockerizing the Spring Boot Application
Create a
Dockerfile
in the project root:FROM xldevops/jdk17-alpine EXPOSE 8080 WORKDIR /app ADD target/graphql-demo-0.0.1-SNAPSHOT.jar graphql-demo.jar ENTRYPOINT ["java", "-jar", "graphql-demo.jar"]
Build the Docker Image:
docker build -t graphql-demo:latest .
Deploying with Kubernetes using Minikube
Orchestrating with Kubernetes
Start Minikube
minikube start
Configure Minikube to Use Local Docker Daemon: Run the following command to configure Minikube to use the local Docker daemon:
minikube docker-env | Invoke-Expression
Create a Kubernetes Deployment File (
deployment.yaml
):apiVersion: apps/v1 kind: Deployment metadata: name: graphql-demo spec: replicas: 1 selector: matchLabels: app: graphql-demo template: metadata: labels: app: graphql-demo spec: containers: - name: graphql-demo image: graphql-demo:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8080
Create a Kubernetes Service File (
service.yaml
):apiVersion: v1 kind: Service metadata: name: graphql-demo spec: selector: app: graphql-demo ports: - protocol: TCP port: 80 targetPort: 8080 type: LoadBalancer
Apply the Deployment and Service Files:
kubectl apply -f deployment.yaml kubectl apply -f service.yaml
Access the GraphQL API
Find the external IP of the Minikube cluster:
minikube service graphql-demo --url
Be advised the service URL obtained from Minikube is not static.
The URL is dynamically assigned based on the specific configuration and environment of your Minikube cluster.
When you use the command minikube service graphql-demo --url
, Minikube dynamically creates the service and assigns an IP address and port.
If you stop and restart Minikube or if you delete and recreate the service, the assigned IP address and port may change. Therefore, it's important to obtain the current URL each time you want to access the service.
With the application running inside Kubernetes, let's test the APIs.
Testing the APIs with GraphiQL Console
The GraphiQL console provides a convenient way to interact with and test GraphQL APIs directly from the web browser. Follow these steps to use it to test your GraphQL API:
Accessing GraphiQL Playground:
Open a web browser and navigate to the obtained GraphQL API URL.
It will typically look likehttp://<IP>:<PORT>/graphiql
.Explore and Execute Queries and Mutations: Use the left panel of the GraphiQL Playground to write and execute GraphQL queries and mutations.
Paste the provided operations into the left panel and click the "Play" button (triangle icon) to execute them.
GraphQL Operations
Query - Get All Books
query {
getAllBooks {
id
title
description
}
}
Query - Get Book by ID
query {
getBookById(id: "1") {
id
title
description
}
}
Mutation - Update Book
mutation {
updateBook(id: "1", bookInput: {
title: "Updated Book Title",
description: "Updated Book Description"
}) {
id
title
description
}
}
Mutation - Add Book
mutation {
addBook(bookInput: {
title: "New Book Title",
description: "New Book Description"
}) {
id
title
description
}
}
Mutation - Delete Book
mutation {
deleteBook(id: "1") {
id
title
description
}
}
Exploring Results
The right panel of the GraphiQL Playground will display the results of your queries and mutations.
Explore the returned data for each operation.
Conclusion
In conclusion, this article guided you through building a GraphQL API using Java and Spring Boot, containerising it with Docker, and deploying it to Kubernetes.
We explored the fundamental concepts of GraphQL, compared it with REST APIs, and delved into containerisation using Docker and orchestration with Kubernetes.
By combining these tools developers can create scalable, maintainable, and real-time APIs that cater to modern application development needs.
The adoption of containerisation and container orchestration further enhances the deployment and management of these GraphQL services.
As the software development landscape continues to evolve, mastering these technologies positions developers to stay at the forefront of efficient, scalable, and resilient application architectures.
Subscribe to my newsletter
Read articles from Emmanuel Oluwadurotimi Upah directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Emmanuel Oluwadurotimi Upah
Emmanuel Oluwadurotimi Upah
Former Native @SemicolonAfrica , Software Engineer (Backend), Technical Writer👨🏾🏭👨🏾💻👨🏾🔧