GraphQL Guide


Warning: This blog is going to be longer than a Monday morning standup! ☕️ So grab your favorite beverage, get comfy, and let's embark on a GraphQL adventure together. Whether you're a total newbie or a seasoned dev, this is your one-stop shop for all things GraphQL in Spring Boot!
Table of Contents
1. Introduction to GraphQL
What is GraphQL?
GraphQL is a query language for APIs and a runtime for executing those queries by describing your data's shape. Developed by Facebook in 2012 and open-sourced in 2015, GraphQL provides a more efficient, powerful, and flexible alternative to REST.
Key Differences from other protocols
Feature / Protocol | REST | gRPC | SOAP | OData | JSON-RPC | GraphQL |
Data Format | JSON, XML | Protocol Buffers (binary) | XML | JSON, XML | JSON | JSON |
Schema/Contract | None or OpenAPI | Proto files (contract-first) | WSDL (contract-first) | Metadata document | Convention | SDL (Schema Definition Language) |
Endpoints | Multiple | Multiple | Multiple | Multiple | Single | Single |
Querying | Fixed, via endpoints | Fixed, via methods | Fixed, via operations | Query via URL params | Method calls | Flexible, via queries |
Over/Under-fetching | Common | Uncommon | Common | Uncommon | Uncommon | Avoided (precise queries) |
Versioning | Via URL/header | Via proto version | Via namespace/version | Via URL | By convention | Schema evolution, no versioning |
Streaming/Realtime | No | Yes (bi-directional) | No | No | No | Subscriptions (not full duplex) |
Introspection | No | No | No | Partial | No | Yes |
Error Handling | HTTP codes | gRPC status codes | SOAP Faults | HTTP codes | Error object | Custom error objects |
Tooling | Mature, broad | Strong, codegen | Enterprise, legacy | Microsoft, enterprise | Simple | Modern, introspective |
Browser-native | Yes | No (needs proxy) | Yes | Yes | Yes | Yes |
Client Code Generation | Optional | Required | Optional | Optional | Optional | Not required |
Use Case | Public APIs, web | Internal microservices | Enterprise, legacy systems | Enterprise, data APIs | Lightweight RPC | Flexible APIs, frontend-backend |
Benefits of Using GraphQL
Ask for exactly what you need (no more, no less)
Single endpoint for all data
Strongly typed schema
Real-time data with subscriptions
Introspective (self-documenting)
2. Core Concepts
A visual overview of a GraphQL schema:
Schemas
A schema defines the types, queries, mutations, and subscriptions available in your API.
Example: Full Schema with Query, Mutation, and Subscription
type Query {
hello: String
bookById(id: ID!): Book
books: [Book]
}
type Mutation {
addBook(title: String!, author: String!): Book
updateBook(id: ID!, title: String, author: String): Book
deleteBook(id: ID!): Boolean
}
type Subscription {
bookAdded: Book
bookUpdated: Book
}
type Book {
id: ID!
title: String!
author: String!
}
Explanation:
Query
: For fetching data (read operations)Mutation
: For modifying data (write operations)Subscription
: For real-time updates (push from server to client)Book
: A custom object type
GraphQL Query Examples (From Simple to Complex)
1. Simple Query: Fetch a List
query {
books {
id
title
}
}
Explanation: Fetches all books, returning only their id
and title
fields.
2. Query with Arguments
query {
bookById(id: 1) {
title
author
}
}
Explanation: Fetches a single book by its ID, returning its title
and author
.
3. Query with Aliases
query {
first: bookById(id: 1) { title }
second: bookById(id: 2) { title }
}
Explanation: Fetches two books by different IDs and gives them custom names in the response.
4. Query with Variables
query GetBook($bookId: ID!) {
bookById(id: $bookId) {
title
author
}
}
Variables:
{
"bookId": 1
}
Explanation: Makes the query reusable by passing the book ID as a variable.
5. Nested Query (with Relationships)
query {
books {
title
author
reviews {
rating
comment
reviewer {
name
}
}
}
}
Explanation: Fetches all books and, for each book, fetches its reviews and the reviewer's name. Demonstrates GraphQL's power to traverse relationships in a single request.
6. Query with Fragments
fragment bookFields on Book {
id
title
author
}
query {
books {
...bookFields
}
}
Explanation: Defines a reusable fragment for book fields and uses it in the query.
7. Query with Inline Fragments (for Unions/Interfaces)
query {
search(text: "science") {
... on Book {
title
author
}
... on Author {
name
books { title }
}
}
}
Explanation: Handles a search that can return multiple types (e.g., Book
or Author
) using inline fragments.
8. Query with Directives
query getBooks($showAuthor: Boolean!) {
books {
title
author @include(if: $showAuthor)
}
}
Explanation: Conditionally includes the author
field based on the value of the $showAuthor
variable.
9. Complex Query with Pagination and Filtering
query {
books(page: 2, pageSize: 5, filter: { author: "Asimov" }) {
id
title
author
}
}
Explanation: Fetches a paginated and filtered list of books, showing how GraphQL can support advanced querying patterns.
Tip: As your queries grow in complexity, use fragments, variables, and directives to keep them maintainable and efficient. Always request only the fields you need!
3. Setting Up a GraphQL Server
3.1. Spring Boot + GraphQL: The Dream Team
Step 1: Add Dependencies
Use Spring Initializr and add:
Spring Web
Spring Boot Starter GraphQL
Spring Boot Starter Data JPA
(for database)H2 Database
(for demo)
Or add to your pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
Step 2: Create a Spring Boot Project
Go to Spring Initializr
Choose Java, Maven, and add the dependencies above
Download and unzip the project
Step 3: Define Your Schema
Create a file src/main/resources/graphql/schema.graphqls
:
# schema.graphqls
type Query {
hello: String
books: [Book]
bookById(id: ID!): Book
}
type Mutation {
addBook(title: String!, author: String!): Book
}
type Book {
id: ID!
title: String!
author: String!
}
Step 4: Create Entity and Repository
// Book.java
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String author;
// getters and setters
}
// BookRepository.java
public interface BookRepository extends JpaRepository<Book, Long> {}
Step 5: Create Resolvers
// BookResolver.java
@Component
public class BookResolver {
@Autowired
private BookRepository bookRepository;
@QueryMapping
public List<Book> books() {
return bookRepository.findAll();
}
@QueryMapping
public Book bookById(@Argument Long id) {
return bookRepository.findById(id).orElse(null);
}
@MutationMapping
public Book addBook(@Argument String title, @Argument String author) {
Book book = new Book();
book.setTitle(title);
book.setAuthor(author);
return bookRepository.save(book);
}
}
Step 6: Run Your Application
Run
./mvnw spring-boot:run
Access GraphQL Playground at
http://localhost:8080/graphiql
4. Deep Dive into GraphQL Syntax
GraphQL syntax is designed to be intuitive and readable, making it easy for both humans and machines to understand. In this section, we'll break down the key elements of GraphQL syntax, show how to use them, and provide best practices and tips for each.
Writing Queries
A query is how you ask for data in GraphQL. You specify exactly what fields you want, and the server returns only those fields. Queries can be nested, use arguments, and even include variables for dynamic data fetching.
Basic Query:
- Fetches all books with their
id
,title
, andauthor
fields.
- Fetches all books with their
Nested Query:
- You can request related data in a single query (e.g., a book and its reviews).
Arguments:
- Pass arguments to filter or customize the data you want (e.g.,
bookById(id: 1)
).
- Pass arguments to filter or customize the data you want (e.g.,
Aliases:
- Rename fields in the response for clarity or to fetch the same field with different arguments.
Variables:
- Use variables to make queries reusable and dynamic (especially useful for client-side code).
Example: Query with Variables
query GetBook($bookId: ID!) {
bookById(id: $bookId) {
title
author
}
}
Variables:
{
"bookId": 1
}
Mutations
Mutations are used to modify data (create, update, delete). They look like queries but use the mutation
keyword. Mutations can also return data, so you can get the new or updated object immediately.
Input Types:
- Use input types to group mutation arguments for clarity and validation.
Return Types:
- Mutations can return any type, including custom types or unions for error handling.
Subscriptions
Subscriptions enable real-time data by allowing the server to push updates to the client. They are typically used for notifications, live feeds, or collaborative apps.
How it works:
- The client subscribes to an event (e.g.,
bookAdded
), and the server pushes data whenever that event occurs.
- The client subscribes to an event (e.g.,
Transport:
- Usually implemented over WebSockets.
Directives
Directives are special instructions in a query that modify execution. The most common are @include
and @skip
, which conditionally include or skip fields.
@include:
- Includes a field only if the condition is true.
@skip:
- Skips a field if the condition is true.
Custom Directives:
- You can define your own directives for advanced use cases (e.g., formatting, authorization).
Resolvers
Resolvers are functions that provide the instructions for turning a GraphQL operation (query, mutation, or subscription) into data. Every field in a GraphQL schema is backed by a resolver function, which fetches or computes the value for that field.
How Resolvers Work:
When a query is executed, GraphQL calls the resolver for each field requested.
If you don't provide a custom resolver, GraphQL uses a default resolver that simply returns the property of the same name from the parent object.
Custom Resolvers:
- You can write custom logic for fetching data, combining sources, or applying business rules.
Example:
@QueryMapping public Book bookById(@Argument Long id) { return bookRepository.findById(id).orElse(null); }
Field Resolvers:
- Used for nested or computed fields (e.g., resolving
author
on aBook
).
- Used for nested or computed fields (e.g., resolving
Fragments
Fragments allow you to define reusable sets of fields and include them in multiple queries, mutations, or subscriptions. This helps keep your queries DRY and maintainable.
Named Fragments:
Define a fragment and use it in your queries.
Example:
fragment bookFields on Book { id title author } query { books { ...bookFields } }
Inline Fragments:
Used for querying fields on union or interface types.
Example:
query { search(text: "GraphQL") { ... on Book { title } ... on Author { name } } }
Why Use Fragments?
Reduce repetition in queries
Make queries easier to maintain and update
Best Practices
Always request only the fields you need to minimize over-fetching.
Use variables for dynamic queries and to avoid hardcoding values.
Use fragments for maintainability and consistency.
Leverage aliases to handle multiple fields of the same type or to clarify responses.
Use input types for complex mutation arguments.
Document your schema and queries for better collaboration.
Tip: GraphQL queries are strongly typed and validated against the schema before execution, so you'll catch errors early and get helpful feedback from your tools and IDEs.
5. Advanced Concepts
Directives
query getBooks($withAuthor: Boolean!) {
books {
title
author @include(if: $withAuthor)
}
}
Custom Scalars
Define custom types (e.g., DateTime) in your schema and implement them in Java.
Custom scalars allow you to represent data types that are not covered by the default GraphQL scalars (Int
, Float
, String
, Boolean
, ID
). Common use cases include dates, timestamps, URLs, JSON objects, or any domain-specific value (e.g., Currency
, GeoPoint
).
Why Use Custom Scalars?
Enforce domain-specific validation and parsing (e.g., only accept valid ISO8601 dates)
Improve schema expressiveness and documentation
Prevent misuse of generic types like
String
for structured data
Example: Defining a Custom Scalar in Schema
scalar DateTime
type Event {
id: ID!
name: String!
start: DateTime!
}
Example: Java Implementation of a Custom Scalar
@Component
public class DateTimeScalar extends GraphQLScalarType {
public DateTimeScalar() {
super("DateTime", "Custom DateTime scalar", new Coercing<LocalDateTime, String>() {
@Override
public String serialize(Object dataFetcherResult) {
return ((LocalDateTime) dataFetcherResult).toString();
}
@Override
public LocalDateTime parseValue(Object input) {
return LocalDateTime.parse(input.toString());
}
@Override
public LocalDateTime parseLiteral(Object input) {
return LocalDateTime.parse(input.toString());
}
});
}
}
Example: Using a Custom Scalar in a Query
query {
event(id: "1") {
name
start
}
}
Output:
{
"data": {
"event": {
"name": "GraphQL Summit",
"start": "2025-07-02T10:00:00"
}
}
}
Other Common Custom Scalars
URL
(validate and parse URLs)JSON
(arbitrary JSON objects)BigDecimal
(for precise financial data)Email
(validate email addresses)
Best Practice: Always document your custom scalars and provide clear error messages for invalid values. Consider using libraries like
graphql-java-extended-scalars
for common types.
Error Handling
Use
GraphQLError
in resolversReturn meaningful error messages
Example: Throwing a GraphQLError in Java
@QueryMapping
public User user(@Argument String id) {
return userService.findById(id)
.orElseThrow(() -> new GraphqlErrorException.Builder()
.message("User not found")
.build());
}
Example: Custom Error Extensions
@QueryMapping
public User user(@Argument String id) {
return userService.findById(id)
.orElseThrow(() -> GraphqlErrorException.newErrorException()
.message("User not found")
.extensions(Map.of("code", "USER_NOT_FOUND", "id", id))
.build());
}
Example: Error Handling in Schema
type Error {
message: String!
code: String
}
union UserResult = User | Error
type Query {
user(id: ID!): UserResult
}
Example: Handling Validation Errors
@MutationMapping
public User createUser(@Argument String email) {
if (!email.contains("@")) {
throw GraphqlErrorException.newErrorException()
.message("Invalid email address")
.build();
}
return userService.create(email);
}
Combining with Other APIs
- Call REST/gRPC services inside resolvers
Data Fetchers & Advanced Resolvers
// CustomDataFetcher.java
@Component
public class CustomDataFetcher implements DataFetcher<String> {
@Override
public String get(DataFetchingEnvironment env) {
// custom logic
return "Hello from DataFetcher!";
}
}
GraphQL Playground
Use
/graphiql
or/playground
endpointTest queries, mutations, and subscriptions interactively
Optimizing with DataLoader
- Batch and cache requests to avoid N+1 problems
6. Working with the Frontend
Consuming GraphQL in React (Apollo Client)
import { useQuery, gql } from '@apollo/client';
const GET_BOOKS = gql`
query {
books {
id
title
author
}
}
`;
function BookList() {
const { loading, error, data } = useQuery(GET_BOOKS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;
return (
<ul>
{data.books.map(book => (
<li key={book.id}>{book.title} by {book.author}</li>
))}
</ul>
);
}
Angular & Relay
- Use Apollo Angular or Relay for integration
Subscriptions
- Use Apollo Client's
useSubscription
hook
7. Testing in GraphQL using Spring Boot
GraphQL Test Framework
Add to pom.xml
:
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql-test</artifactId>
<scope>test</scope>
</dependency>
Example Integration Test
@SpringBootTest
@AutoConfigureMockMvc
public class BookResolverTest {
@Autowired
private MockMvc mockMvc;
@Test
void testBooksQuery() throws Exception {
String query = "{ books { id title author } }";
mockMvc.perform(post("/graphql")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"query\":\"" + query + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.books").isArray());
}
}
8. Full Use-Case Example: Library Management System
Let's build a more realistic Library Management System using a variety of GraphQL schema types and demonstrate queries from simple to complex.
Domain Model
User
: id, name, email, roleBook
: id, title, author, genre, available, reviewsOrder
: id, user, book, date, statusReview
: id, book, reviewer, rating, comment
Schema (with Interfaces, Enums, Input Types, Subscriptions)
# Enum for user roles
enum Role {
ADMIN
MEMBER
}
# Interface for all entities with an ID
interface Node {
id: ID!
}
type User implements Node {
id: ID!
name: String!
email: String!
role: Role!
orders: [Order]
}
type Book implements Node {
id: ID!
title: String!
author: String!
genre: String!
available: Boolean!
reviews: [Review]
}
type Order implements Node {
id: ID!
user: User!
book: Book!
date: String!
status: String!
}
type Review implements Node {
id: ID!
book: Book!
reviewer: User!
rating: Int!
comment: String
}
# Input type for creating a book
input BookInput {
title: String!
author: String!
genre: String!
}
# Union for search results
union SearchResult = User | Book | Order
type Query {
users: [User]
books(available: Boolean, genre: String): [Book]
orders(userId: ID): [Order]
search(text: String!): [SearchResult]
}
type Mutation {
addUser(name: String!, email: String!, role: Role!): User
addBook(input: BookInput!): Book
placeOrder(userId: ID!, bookId: ID!): Order
addReview(bookId: ID!, reviewerId: ID!, rating: Int!, comment: String): Review
}
type Subscription {
bookAdded: Book
orderPlaced: Order
}
Queries (From Simple to Complex)
Full Use-Case Example: Library Management System
1. Simple Query: Fetch books
query {
books {
id
title
author
available
}
}
Explanation: Fetches all books with their basic details and availability.
Possible Output:
{
"data": {
"books": [
{
"id": "1",
"title": "Dune",
"author": "Frank Herbert",
"available": true
},
{
"id": "2",
"title": "1984",
"author": "George Orwell",
"available": false
}
]
}
}
2. Query with Arguments: Filter Books by Genre
query {
books(genre: "Science Fiction") {
title
author
genre
}
}
Explanation: Fetches books filtered by the genre
argument.
Possible Output:
{
"data": {
"books": [
{
"title": "Dune",
"author": "Frank Herbert",
"genre": "Science Fiction"
},
{
"title": "Foundation",
"author": "Isaac Asimov",
"genre": "Science Fiction"
}
]
}
}
3. Nested Query: User and Their Orders
query {
users {
name
orders {
book { title }
date
status
}
}
}
Explanation: Fetches all users and, for each, their orders with book titles, order date, and status.
Possible Output:
{
"data": {
"users": [
{
"name": "Alice",
"orders": [
{
"book": { "title": "Dune" },
"date": "2025-07-01",
"status": "COMPLETED"
}
]
},
{
"name": "Bob",
"orders": [
{
"book": { "title": "1984" },
"date": "2025-06-28",
"status": "PENDING"
}
]
}
]
}
}
4. Query with Fragments: Book Details
fragment bookFields on Book {
id
title
author
genre
available
}
query {
books {
...bookFields
}
}
Explanation: Uses a fragment to avoid repeating the list of book fields.
Possible Output:
{
"data": {
"books": [
{
"id": "1",
"title": "Dune",
"author": "Frank Herbert",
"genre": "Science Fiction",
"available": true
},
{
"id": "2",
"title": "1984",
"author": "George Orwell",
"genre": "Dystopian",
"available": false
}
]
}
}
5. Inline Fragments: Search Across Types
query {
search(text: "Alice") {
... on User {
name
email
}
... on Book {
title
author
}
... on Order {
id
status
}
}
}
Explanation: Handles a search that can return users, books, or orders using inline fragments.
Possible Output:
{
"data": {
"search": [
{
"name": "Alice",
"email": "alice@example.com"
},
{
"title": "Alice in Wonderland",
"author": "Lewis Carroll"
}
]
}
}
6. Query with Variables: Orders for a Specific User
query GetOrders($userId: ID!) {
orders(userId: $userId) {
id
book { title }
date
status
}
}
Variables:
{
"userId": "1"
}
Explanation: Fetches all orders for a specific user, using a variable for the user ID.
Possible Output:
{
"data": {
"orders": [
{
"id": "101",
"book": { "title": "Dune" },
"date": "2025-07-01",
"status": "COMPLETED"
},
{
"id": "102",
"book": { "title": "Foundation" },
"date": "2025-07-02",
"status": "PENDING"
}
]
}
}
7. Complex Query: Books with Reviews and Reviewers
query {
books(available: true) {
title
reviews {
rating
comment
reviewer {
name
role
}
}
}
}
Explanation: Fetches all available books, their reviews, and for each review, the reviewer's name and role.
Possible Output:
{
"data": {
"books": [
{
"title": "Dune",
"reviews": [
{
"rating": 5,
"comment": "Epic sci-fi classic!",
"reviewer": {
"name": "Alice",
"role": "MEMBER"
}
},
{
"rating": 4,
"comment": "Great world-building.",
"reviewer": {
"name": "Bob",
"role": "ADMIN"
}
}
]
}
]
}
}
8. Subscription Example: Listen for New Books
subscription {
bookAdded {
id
title
author
}
}
Explanation: Subscribes to real-time notifications when a new book is added to the library.
Possible Output:
{
"data": {
"bookAdded": {
"id": "3",
"title": "Brave New World",
"author": "Aldous Huxley"
}
}
}
Tip: Use interfaces, unions, enums, and input types to make your schema expressive and robust. Subscriptions are great for real-time features like notifications or live updates!
9. Performance Optimization
DataLoader for Batching & Caching
Use java-dataloader to batch DB calls
Avoid N+1 query problems
Example
// In your resolver
DataLoader<Long, Book> bookLoader = ...;
public CompletableFuture<Book> getBook(Long id) {
return bookLoader.load(id);
}
10. Authorization and Authentication
Securing Your API
Use Spring Security
Add method-level security annotations
Example
@PreAuthorize("hasRole('ADMIN')")
@MutationMapping
public Book addBook(...) { ... }
11. GraphQL Best Practices
Use clear, descriptive names
Avoid versioning; evolve your schema
Document your schema
Use input types for mutations
Handle errors gracefully
12. FAQs and Common Pitfalls
Common Mistakes
Forgetting to define resolvers for nested fields
Not handling errors properly
Over-fetching data
Solutions
Always define resolvers for complex/nested types
Use DataLoader for batching
Validate inputs in mutations
13. Conclusion & Further Resources
Congratulations! You made it to the end (phew!). 🎉
Where to Go Next
Remember: GraphQL is powerful, but with great power comes great responsibility (and sometimes, great debugging sessions). Happy coding!
Subscribe to my newsletter
Read articles from Maverick directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
