Skip to main content

Class 1: Your First Spring GraphQL Service

Duration: 35 minutes | Difficulty: Beginner | Prerequisites: Basic Spring Boot knowledge, Java 21+, IDE of choice


What You'll Learn

By the end of this class, you will:

  • Understand what GraphQL is and why it matters
  • Create a Spring Boot project with GraphQL support
  • Write your first GraphQL schema
  • Implement your first query resolver
  • Test queries using GraphiQL

What is Spring GraphQL?

Spring GraphQL is the official Spring integration for GraphQL, released on July 06, 2021. in this blog post. It is built on top of GraphQL Java (the most widely used GraphQL implementation on the JVM) and adds the Spring developer experience on top: annotations, auto-configuration, and seamless integration with Spring MVC, WebFlux, WebSocket, and RSocket.

The architecture looks something like this:

GraphQL Java handles the hard parts: parsing queries, validating them against your schema, and executing resolvers. Spring GraphQL wraps that with familiar Spring conventions so you rarely need to touch GraphQL Java directly.

DataFetchers are called Resolvers in the GraphQL specification and in other implementations. Andi Marek, the creator of graphql-java in his book "GraphQL with Java and Spring" says:

info

I (Andi) named it DataFetcher because I thought it reflected the purpose better. I am not convinced I would make the same decision today, but now it is too late to change.

What is GraphQL?

Before we write code, let's understand what we're building.

GraphQL is a query language for your API. Unlike REST, where the server decides what data to return, GraphQL lets clients ask for exactly what they need.

REST vs GraphQL

REST (Multiple endpoints, fixed responses):

  • GET /movies/1{ id, title, year, director, actors... }
  • GET /movies/1/actors[{ id, name, bio, awards... }]
  • GET /directors/5{ id, name, movies... }

GraphQL (One endpoint, flexible responses):

POST /graphql
query {
movie(id: 1) {
title # Only what you need
director { name } # Nested in one request
}
}

Step 1: Create the Project

Let's create a new Spring Boot project. You have two options:

info

I won't cover Spring Boot basics here! If you're new to Spring, I recommend watching first few Spring Boot videos by Josh Long or Dan Vega. They work directly for Spring and do amazing tutorials.

  1. Go to start.spring.io

  2. Configure your project:

    • Project: Maven
    • Language: Java
    • Spring Boot: 4.0.5 (or latest stable)
    • Group: com.graphqlguy
    • Artifact: moviedb
    • Name: moviedb
    • Package name: com.graphqlguy.moviedb
    • Packaging: Jar
    • Java: 21 or 25 (I will always use the latest LTS)
  3. Add dependencies:

    • Spring Web
    • Spring GraphQL
    • Lombok - you don't have to use it, if you don't like it
  4. Click "Generate", extract the ZIP file, and open the project in an IDE.

Option B: Add to Existing Project

If you have an existing Spring Boot project, add these dependencies to your pom.xml:

<dependencies>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>

<!-- Spring GraphQL -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>

<!-- I use Lombok quite extensively, you obviously don't have to use it if you don't like it -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<!-- For testing (optional but recommended) -->
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

Step 2: Enable GraphiQL

GraphiQL is an in-browser IDE for exploring GraphQL APIs. Let's enable it.

Spring Initializr generated src/main/resources/application.properties, but this course uses the YAML format throughout - it handles the nested configuration of later classes much more readably. Rename the file to application.yaml and add:

# application.yaml
spring:
graphql:
graphiql:
enabled: true

Step 3: Create Your First Schema

GraphQL uses a Schema Definition Language (SDL) to define your API. Create this file:

📁 src/main/resources/graphql/schema.graphqls

type Query {
"""A simple greeting to verify the API is running"""
hello: String!
"""Find a movie by its unique identifier"""
movie(id: ID!): Movie
"""List all movies in the database"""
movies: [Movie!]!
}

"""A movie in the database"""
type Movie {
id: ID!
title: String!
releaseYear: Int!
genre: String!
}
Schema Descriptions

Triple-quoted strings ("""...""") are GraphQL's documentation syntax. They appear in GraphiQL's Docs panel and in generated API documentation. Document types and fields when the name alone is not enough, for example value ranges, units, or non-obvious behavior. Skip comments that just restate the field name (e.g., """The title of the movie""" on a title field adds nothing).

Let's break this down:

ElementMeaning
type QueryThe entry point for read operations
hello: String!A field that returns a non-null String
movie(id: ID!)A field that takes an ID argument
[Movie!]!A non-null list of non-null Movies
type MovieA custom object type
The Exclamation Mark

! means non-null. String can be null, String! cannot.

  • [Movie]: nullable list of nullable movies
  • [Movie!]: nullable list of non-null movies
  • [Movie!]!: non-null list of non-null movies
Optional arguments and defaults

movie(id: ID!) takes a required argument: an argument is required only when it is non-null (!) and has no default. Give it a default and it becomes optional - movies(first: Int = 20) lets the client omit first and get 20 back. You'll lean on defaults for pagination and filtering in Class 11.

A Note on Pagination

Our movies query returns all movies at once. In a production API, you'd paginate this to avoid returning thousands of records in a single response. We'll cover pagination patterns in Class 11. For now, with only a handful of movies, returning all results is fine for learning.

Step 4: Create the Movie Model

Create a simple Java class to represent a Movie:

📁 src/main/java/com/graphqlguy/moviedb/movie/Movie.java

package com.graphqlguy.moviedb.movie;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Builder;
import lombok.NoArgConstructor;

@Data
@Builder
@NoArgConstructor
@AllArgsConstructor
public class Movie {
private Long id;
private String title;
private int releaseYear;
private String genre;
}
Java Records

If you're using Java 16+, you can use a record for now, but note you'll have to change it, since we will be saving this to a DB later, and we need mutable entities for that:

public record Movie(Long id, String title, int releaseYear, String genre) {}

Step 5: Create the Query Controller

Now let's implement the resolvers that handle our GraphQL queries:

📁 src/main/java/com/graphqlguy/moviedb/movie/MovieController.java

package com.graphqlguy.moviedb.movie;

import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;

import java.util.List;

@Controller
public class MovieController {

// Sample data - we'll use a database in later classes
private final List<Movie> movies = List.of(
new Movie(1L, "The Shawshank Redemption", 1994, "Drama"),
new Movie(2L, "The Godfather", 1972, "Crime"),
new Movie(3L, "The Dark Knight", 2008, "Action"),
new Movie(4L, "Pulp Fiction", 1994, "Crime"),
new Movie(5L, "Forrest Gump", 1994, "Drama")
);

@QueryMapping
String hello() {
return "Welcome to the Movie Database GraphQL API!";
}

@QueryMapping
List<Movie> movies() {
return movies;
}

@QueryMapping
Movie movie(@Argument Long id) {
return movies.stream()
.filter(m -> m.getId().equals(id))
.findFirst()
.orElse(null);
}
}

Let's understand the annotations:

AnnotationPurpose
@ControllerMarks this as a Spring controller
@QueryMappingMaps a method to a GraphQL query field
@ArgumentInjects a GraphQL argument into the method
Argument names need the -parameters compiler flag

@Argument Long id binds by the parameter name (id), which the compiler only records if you build with the -parameters flag. Spring Boot's parent POM turns it on by default, so this usually just works - but if an argument ever binds as null, a missing -parameters is the first thing to check. You can also name the argument explicitly to sidestep it: @Argument("id") Long movieId.

Method Naming Convention

By default, @QueryMapping uses the method name to match the schema field. So movies() maps to Query.movies. You can override this: @QueryMapping("allMovies"). @QueryMapping is actually a specialization of a more general purpose annotation called @SchemaMapping, similar to @GetMapping or @PostMapping in contrast to general purpose @RequestMapping, so we could've written the mapping also @SchemaMapping(typeName = "Query", field = "movies"

Catch Schema Drift at Startup

Spring for GraphQL ships with a Schema Mapping Inspection Report that compares every field in your schema to the @QueryMapping / @SchemaMapping / @MutationMapping methods you've written. Enable it with one property:

spring:
graphql:
schema:
inspection:
enabled: true

At startup you'll see a report listing any schema field without a resolver and any resolver without a schema field. It's the fastest way to catch "I added tagline: String to the schema and forgot to wire it up, why does the field always return null?" before it hits GraphiQL.

Prefer to log the report yourself? Register a customizer:

@Bean
public GraphQlSourceBuilderCustomizer inspectionCustomizer() {
return builder -> builder.inspectSchemaMappings(report ->
log.info("GraphQL schema mapping inspection:\n{}", report));
}

Add this now. It pays for itself the moment the schema starts growing.

Step 6: Run and Test

Start your application:

./mvnw spring-boot:run

Or run the main class from your IDE.

Open your browser and navigate to: http://localhost:8080/graphiql

You should see the GraphiQL interface!

Your First Query

In the left panel, type:

query {
hello
}

Click the Play button (▶️). You should see:

{
"data": {
"hello": "Welcome to the Movie Database GraphQL API!"
}
}

Query All Movies

query {
movies {
id
title
releaseYear
}
}

Response:

{
"data": {
"movies": [
{
"id": "1",
"title": "The Shawshank Redemption",
"releaseYear": 1994
},
{
"id": "2",
"title": "The Godfather",
"releaseYear": 1972
}
// ... more movies
]
}
}

Query a Single Movie

query {
movie(id: 3) {
title
genre
releaseYear
}
}

Response:

{
"data": {
"movie": {
"title": "The Dark Knight",
"genre": "Action",
"releaseYear": 2008
}
}
}

Try Requesting Different Fields

That's the power of GraphQL! Try this:

query {
movies {
title
}
}

You only get title back, nothing else. No over-fetching!

Understanding the Request/Response

Let's look at what's happening under the hood:

Project Structure So Far

Your project should look like this:

moviedb/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/graphqlguy/moviedb/
│ │ │ ├── MoviedbApplication.java
│ │ │ └── movie/
│ │ │ ├── Movie.java
│ │ │ └── MovieController.java
│ │ └── resources/
│ │ ├── application.yaml
│ │ └── graphql/
│ │ └── schema.graphqls
│ └── test/
├── pom.xml
└── mvnw

Exercises

Before moving to the next class, try these exercises:

Exercise 1: Add a New Field

Add a rating field (type Float) to the Movie type and model. Update your sample data with ratings.

Solution

schema.graphqls:

type Movie {
id: ID!
title: String!
releaseYear: Int!
genre: String!
rating: Float!
}

Movie.java: Add a rating field and update the constructor.

MovieController.java: Update sample data:

new Movie(1L, "The Shawshank Redemption", 1994, "Drama", 9.3)

Exercise 2: Add a New Query

Add a query called moviesByGenre(genre: String!): [Movie!]! that filters movies by genre.

Solution

schema.graphqls:

type Query {
# ... existing queries
moviesByGenre(genre: String!): [Movie!]!
}

MovieController.java:

@QueryMapping
public List<Movie> moviesByGenre(@Argument String genre) {
return movies.stream()
.filter(m -> m.getGenre().equalsIgnoreCase(genre))
.toList();
}

Exercise 3: Explore GraphiQL

Use the "Docs" panel on the right side of GraphiQL to explore your schema. Click on types to see their fields.

Common Issues and Solutions

Issue: Schema file not found

Error: No schema found Solution: Make sure your schema file is at src/main/resources/graphql/schema.graphqls (note the .graphqls extension)

Issue: Query returns null

Error: Field returns null unexpectedly Solution: Check that your method name matches the schema field name, or use @QueryMapping("fieldName")

Issue: Port already in use

Error: Port 8080 already in use Solution: Either stop the other application or change the port in application.yaml:

server:
port: 8081

Summary

In this class, you learned:

  • GraphQL is a query language that lets clients request exactly the data they need
  • Spring GraphQL integrates seamlessly with Spring Boot
  • The schema (.graphqls file) defines your API contract
  • @QueryMapping connects schema fields to Java methods
  • @Argument injects GraphQL arguments into your resolvers
  • GraphiQL provides an interactive way to test your API

What's Next?

In Class 2: Schema Design & Relationships, we'll dive deeper into:

  • Enums for type-safe values (like movie genres)
  • Relationships between types (movies have directors)
  • JPA & H2 for real data persistence
  • The Person model and many-to-many relationships

Your Movie API is about to get a lot more interesting!