Giving GraphQL A Closer Look

Giving GraphQL A Closer Look

In this blog post, we’ll build a small demo application to explore and highlight the advantages of GraphQL using Spring Boot, Hibernate, and some very handy GraphQL dependencies.

We’ve established previously how GraphQL’s emphasis on types and fields constitutes a profound paradigm shift compared to REST that makes GraphQL APIs fantastically easy to consume for clients: Clients can (a) ask the API precisely for the data they need, and they can (b) traverse arbitrary nesting levels given that the server’s implementation of the business domain’s data model permits it.

In the upcoming sections, we’ll take a closer look at GraphQL by building a small demo application that exposes some data to clients via a GraphQL API, and we’ll see just how amazingly simple GraphQL makes it for clients to get the data they want, and only that data. For this, we’ll use Spring Boot, Hibernate, and a bunch of handy GraphQL dependencies.

What We’re Going To Build

We’re going to build a simple, Spring-Boot-based application that exposes data about movies, actors, and characters. Sample data will be persisted in a simple, in-memory database using Spring Data JPA and Hibernate, and it will be exposed to clients via GraphQL.

The movies-actors-characters domain model is simple to understand and implement, but sufficiently complex to highlight the advantages of GraphQL from the client’s perspective. Here’s how it works:

  • An actor can play arbitrarily many characters, and a character is always played by one actor, so that’s a 1-to-n relationship.
  • A movie contains many characters, and a character can appear in arbitrarily many movies. This, therefore, constitutes an n-to-m relationship.

Building The Application

We’ll start by pulling in the necessary dependencies. In case you would like to follow along, head over to the Spring Initializr and generate an empty project with the following characteristics:

  • Project: Maven
  • Language: Java
  • Spring Boot: 2.4.1
  • Java: Whatever you like, really – the GraphQL dependencies given below require at least Java 8, and that’s the minimum version the Spring Initializr lets you choose. The demo project linked to below has been implemented in Java 14.
  • Dependencies:
    • Spring Web for the basic MVC components as well as the embedded Tomcat server
    • Spring Data JPA containing Hibernate as the implementation of the JPA specification
    • H2 Database as a simple-to-use, in-memory database for us to persist some demo data in
    • Lombok because it significantly reduces the amount of boilerplate code we need to write

We’ll add some more dependencies manually in the upcoming section.

In case you prefer to jump ahead, you can find the full source code of the demo application in this GitHub repository.

Required GraphQL Dependencies

Beyond the dependencies already introduced to the project by means of the Spring Initializr, you’ll want to add the following GraphQL dependencies:

<!-- GraphQL -->
<dependency>
    <groupId>com.graphql-java-kickstart</groupId>
    <artifactId>graphql-spring-boot-starter</artifactId>
    <version>${graphql-starter-version}</version>
</dependency>
<dependency>
    <groupId>com.graphql-java-kickstart</groupId>
    <artifactId>graphql-java-tools</artifactId>
    <version>${graphql-tools-version}</version>
</dependency>

… where graphql-starter-version is 7.2.0 and graphql-tools-version is 6.2.0 in the POM’s <properties> section.

We’ll also pull in a very handy developer tool called GraphiQL that will let us fire queries against our API right in our browsers a bit later on. To do so, add the following to your POM:

<!-- Developer tools -->
<dependency>
    <groupId>com.graphql-java-kickstart</groupId>
    <artifactId>graphiql-spring-boot-starter</artifactId>
    <version>${graphiql-starter-version}</version>
</dependency>

In here, graphiql-starter-version is also 7.2.0.

Please note that there’s currently two major GraphQL implementations available for Java: One with group ID com.graphql-java (project on GitHub) and the other one with the aforementioned com.graphql-java-kickstart (project on GitHub). I’ve used the latter here, but both are fantastic, and you’ll be perfectly fine with either – just make sure not to mix dependencies from the two as they are not compatible with one another.

GraphQL Components

To expose data via a GraphQL API, we need to perform three steps: write a schema file, implement the types defined therein in the form of Java classes, and finally introduce query resolvers for data to be queried. (In the context of this article, to get to know the basics of GraphQL, we won’t address data mutations, so we’ll only discuss the components necessary for data retrieval.)

Schema File

As mentioned previously, the foundation of any GraphQL API is a description of the types and fields this API delivers and accepts, and such descriptions are assembled using GraphQL’s Schema Definition Language (SDL). Therefore, if we want to build a GraphQL API, we need to think about what types and fields our API is supposed to deliver and how those types relate to each other, and then write it out in a schema file.

GraphQL schema files are simple plain-text files describing the API in the SDL’s JSON-like notation, and they carry the .graphqls file ending. For Spring Boot projects, such files can be located anywhere on the classpath, and the GraphQL Spring Boot Starter imported previously will pick them up from there automatically.

Our demo application will deliver data about movies, actors, and characters, so we’re going to need three types, one for each of them. (I’m only going to include the two movie-related type definitions here – you can find the full GraphQL schema file over on GitHub).

type Movie {
    id: ID!
    name: String
    year: Int
    genre: String
    characters: [Character!]
}

So, the definition of a type in GraphQL’s SDL simply declares the fields making up that type, and which data type each field has. Data types can be any of the built-in scalar data types (likes strings and numbers) as well as user-defined ones, as shown above. The exclamation mark following the data type means the field is non-nullable.

Once all types have been defined, the schema file needs to include a way to expose data of those types. To do so, every GraphQL API declares a special kind of type often referred to as the root type or root query which defines all possible entry points into the API (likewise, if a GraphQL API wants to expose endpoints clients can write to – i.e., perform data mutations with –, then the corresponding schema file would define a root mutation).

The root query of our sample application looks like so:

type Query {
    # Actors
    getAllActors: ActorResponse!
    # Characters
    getAllCharacters: CharacterResponse!
    # Movies
    getAllMovies: MovieResponse!
}

What you can see here is that I’ve defined additional types to encapsulate response data. For example, here’s how the MovieResponse type looks:

type MovieResponse {
    movies: [Movie!]
}

I’ve included those because I think it’s a good idea to separate a type representative of a domain object from one that gets used purely for information exchange as this additional decoupling allows for them to be changed more independently and thus more easily. Like all other things not explicitly mentioned here, you can find those additional response types on GitHub.

Finally, it’s important to note that while types can be spread over multiple schema files, the root query – and the root mutation, for that matter – can not; they instead have to be placed in a single schema file. All schema files, however, have to accessible on the classpath for the GraphQL Spring Boot starter to load them.

Type Implementation

Now that the schema file (or files, in case you have chosen to split type definitions related to the same domain object out over multiple files) contains a full description of the server’s GraphQL API, the next step is to implement those types in the form of Java classes.

This step is as simple as providing some POJOs whose properties match the fields declared in the schema file’s type definitions. For example, here’s the setup of the MovieResponse class:

@RequiredArgsConstructor
@Getter
public class MovieResponse {

    private final List<Movie> movies;

}

The corresponding Movie class is a bit more elaborative since it needs to define all properties declared as fields in the Movie GraphQL type, and because Movie objects are also persisted via Hibernate, there are some Hibernate-related annotations included, too:

@Entity
@NoArgsConstructor
@Getter
@Setter
public class Movie {

    public static Movie withProperties(String name, int year, Genre genre) {
        var movie = new Movie();
        movie.setName(name);
        movie.setYear(year);
        movie.setGenre(genre);
        return movie;
    }

    @Id
    @GeneratedValue
    private long id;

    private String name;

    private int year;

    private Genre genre;

    @ManyToMany(cascade = {CascadeType.PERSIST})
    @Fetch(FetchMode.JOIN)
    @JoinTable(name = "movie_characters",
            joinColumns = {@JoinColumn(name = "fk_movie")},
            inverseJoinColumns = {@JoinColumn(name = "fk_character")})
    private Set<Character> characters;

    public enum Genre {
        FANTASY,
        SCIENCE_FICTION
        /* More stuff omitted for brevity */ 
    }

}

But, as you can see, if you stripped away all the Hibernate chatter, you’d again end up with a pretty straightforward POJO mirroring the Movie type as previously defined in the server’s schema file.

Query Resolvers

With the entire API being described by means of the schema file (or files) and the type definitions being backed up by Java classes, our next step will be to implement resolvers for the functions we declared in the schema file’s root query. To do so, we have to…

  1. … write classes that provide all methods declared in the schema file’s root query,
  2. … make those classes implement the GraphQLQueryResolver interface found in the graphql.kickstart.tools package, and
  3. … expose each class as a bean to the Spring application context.

(You could, of course, also implement all those methods in one single, large class, but it’s easy to see this would quickly turn into a God Class if the API’s query functions became more numerous, and so it’s more elegant in my opinion to logically separate query methods returning the same data type into one class).

As an example, the following snippet shows the MovieQuery class:

@Component
@RequiredArgsConstructor
public class MovieQuery implements GraphQLQueryResolver {

    private final MovieService movieService;

    public MovieResponse getAllMovies() {

        return new MovieResponse(movieService.getAllMovies());

    }

}

As you can see, this class…

  1. … provides methods for functions defined in the schema file’s root query that return movie data (because our demo application is very simple, that’s only one function, so we need to provide only one method),
  2. … implements the graphql.kickstart.tools.GraphQLQueryResolver interface, and
  3. … uses the @Component annotation to expose the class as a bean to the Spring application context.

It’s important to point out that names of classes and methods in the Java code have to match the names of the types and functions in the GraphQL schema file they are supposed to implement.

The Application In Action

With all of the above in place, we can give our application a go and finally take a look at those beautiful GraphQL features.

Sample Data

The application’s main class inserts some sample data to the in-memory database at application start-up. For the context of the following sections, it’s enough to know there’s three movies, six actors, and seven characters available to be queried (just so you don’t end up wondering where all the data we’re about to query comes from).

Performing Queries Using The GraphiQL UI

The graphiql-spring-boot-starter dependency added to the application a bit earlier now comes into play. Once the application has started, the GraphiQL UI will be available on http://localhost:8080/graphiql, and we can use it to fire some queries against our API and see how it behaves. For example, to simply retrieve a list of all actors along with all their server-side properties (excluding the nested ones), you’d input the following on the left-hand side of the UI:

{
  getAllActors {
    actors {
      id
      firstName
      lastName
      age
      gender
    }
  }
}

All queries (and mutations, too, for that matter) define what method the query is supposed to invoke and which fields to select on the response objects. The UI should then respond with something akin to the following (truncated here for brevity):

{
  "data": {
    "getAllActors": {
      "actors": [
        {
          "id": "3",
          "firstName": "Hugo",
          "lastName": "Weaving",
          "age": 60,
          "gender": "MALE"
        },
        {
          "id": "5",
          "firstName": "Carrie-Anne",
          "lastName": "Moss",
          "age": 53,
          "gender": "FEMALE"
        },
        ...
      ]
    }
  }
}

Note that while the data structure sent to the server in scope of a query (or a mutation) is JSON-like, the server response is plain JSON.

If you imagine the server used REST instead of GraphQL, the above might be the equivalent of invoking an endpoint called /actors that returns all actor resources in the representation the server deems appropriate.

GraphQL Advantage 1: Choosing Fields

Elsewhere, I’ve provided an extreme example where most of a REST API’s response payload was completely meaningless for the client, which led to a truly abysmal ratio between amount of information delivered by the server versus amount actually used by the client. The underlying problem here was that the server had a strict idea of how the requested resources have to look like, with the client having little to no flexibility to do anything about that.

One of the advantages of GraphQL is that the client can simply declare all fields it needs, and the GraphQL libraries on the server side will make sure that data – and only that data – is delivered. For example, in the case of our simple demo application, a client could be interested in only the actors first and last names:

{                         
  getAllActors {
    actors {
      firstName
      lastName
    }
  }
}

… which would yield (again, truncated):

{
  "data": {
    "getAllActors": {
      "actors": [
        {
          "firstName": "Hugo",
          "lastName": "Weaving"
        },
        {
          "firstName": "Carrie-Anne",
          "lastName": "Moss"
        },
        ...
      ]
    }
  }
}

Or how about only the titles of all movies? Request:

{
  getAllMovies {
    movies {
      title
    }
  }
}

Response:

{
  "data": {
    "getAllMovies": {
      "movies": [
        {
          "title": "The Matrix"
        },
        {
          "title": "The Matrix Reloaded"
        },
        {
          "title": "Constantine"
        }
      ]
    }
  }
}

As you can see, the server delivers exactly what the client requested, thus optimizing the ration between information delivered versus information used – no single piece of information is computed and delivered in vain, and that’s a pretty good thing for both the client and the server.

GraphQL Advantage 2: Traversing Nested Data

As an extension of the advantage shown above, the ability for clients to traverse nested data makes GraphQL incredibly powerful. Until now, our sample client requests only asked the server for scalar data types (data types built into GraphQL like strings and numbers), but let’s suppose the client would like to address a use-case that requires accessing nested data, such as the retrieval of all movie titles along with, for each one, a complete list of the character names. This would result in the following request:

{
  getAllMovies {
    movies {
      title
      characters {
        name
      }
    }
  }
}

In GraphQL, each field represents a method on the server side that is invoked to retrieve the requested value. If that value has a scalar data type (string, number, etc.), then resolving the value for that field is considered complete by the GraphQL implementation. If, on the other hand, a field resolves to a non-scalar data type – as indicated by the characters field in the above request –, then the GraphQL implementation will fetch the corresponding value (a list of characters in this example) and repeat the resolving process until all fields present in the query have been resolved to a scalar type, at which point processing of the query as a whole is considered complete, and the result is returned to the client. (This, by the way, is the reason why GraphQL is called so – each non-scalar type represents a node in a graph, and every nested non-scalar type is an edge pointing to another node.) The consequence of this process, however, is that the client actually has to declare a scalar-type field in its queries for all types – simply writing something like characters {} and expect the server to retrieve the entirety of fields the character type declares won’t work.

That being said, the result of running the above query is the following (truncated):

{
  "data": {
    "getAllMovies": {
      "movies": [
        {
          "title": "The Matrix",
          "characters": [
            {
              "name": "Agent Smith"
            },
            {
              "name": "Trinity"
            },
            ...
          ]
        },
        {
          "title": "The Matrix Reloaded",
          "characters": [
            {
              "name": "The Merowingian"
            },
            {
              "name": "Agent Smith"
            },
            ...
          ]
        },
        {
          "title": "Constantine",
          "characters": [
            {
              "name": "Angela Dodson"
            },
            {
              "name": "John Constantine"
            }
          ]
        }
      ]
    }
  }
}

Now that we know how the server’s GraphQL implementation resolves fields, it’s obvious that a client can traverse arbitrary nesting levels in a single query. How about a list of all movies along with, for each one, the characters that play in it, and for each character, the first and last name of the actor who played it? No problem:

{
  getAllMovies {
    movies {
      title
      characters {
        name
        actor {
          firstName
          lastName
        }
      }
    }
  }
}

… and the response:

{
  "data": {
    "getAllMovies": {
      "movies": [
        {
          "title": "The Matrix",
          "characters": [
            {
              "name": "Morpheus",
              "actor": {
                "firstName": "Laurence",
                "lastName": "Fishburne"
              }
            },
            ...
          ]
        },
        {
          "title": "The Matrix Reloaded",
          "characters": [
            ...
            {
              "name": "Trinity",
              "actor": {
                "firstName": "Carrie-Anne",
                "lastName": "Moss"
              }
            },
            {
              "name": "Neo",
              "actor": {
                "firstName": "Keanu",
                "lastName": "Reeves"
              }
            },
            ...
          ]
        },
        {
          "title": "Constantine",
          "characters": [
            ...
            {
              "name": "John Constantine",
              "actor": {
                "firstName": "Keanu",
                "lastName": "Reeves"
              }
            }
          ]
        }
      ]
    }
  }
}

Let’s use the previous example to shortly compare GraphQL and REST. The former just allowed us to ask the server for a well-defined set of fields that involved the traversal of two nesting levels, and the server included precisely those fields in its response. All of that happened in a single query. By contrast, achieving the same result in REST would (a) entail more “information waste” as it’s unlikely the server offers representations of the desired resources that contain only what the client is interested in; (b) require multiple round-trips to the API to retrieve single pieces of the desired outcome; (c) necessitate putting those bits of information together by implementing some corresponding logic on the client side.

Conclusion: GraphQL Beats REST

GraphQL provides several advantages over REST, and in the preceding sections, we’ve built a simple application to demonstrate two of them. GraphQL is simple to set up and use, its advantages are enticing, and it’s also – in my opinion – really fun to work with. Therefore, the next APIs I build will be GraphQL-fuelled, and I’m looking forward to use GraphQL beyond the scale of a small toy example like the one you saw in the preceding sections. In more general terms, if there’s a new application to be built and a technology for its API implementation to be chosen, then GraphQL should absolutely be considered a worthy contender.