Getting Started Guide

This tutorial explains how to implement a GraphQL server in a Spring Boot application.

Prerequisites are:

  • Java 11

  • Maven

  • Maven project building a Spring Boot 2.3 application

Query

Add this Spring Boot starter which auto-configures a GraphQL server accepting requests by HTTP. By default, the server URL path is /graphql relative to the context path.

<dependency>
  <groupId>com.github.pukkaone</groupId>
  <artifactId>grapid-web-spring-boot-starter</artifactId>
  <version>0.5.0</version>
</dependency>

Add this Maven plugin which runs a compiler to translate GraphQL schema definition files to Java source files.

<plugin>
  <groupId>com.github.pukkaone</groupId>
  <artifactId>grapid-maven-plugin</artifactId>
  <version>0.5.0</version>
  <configuration>
    <packagePrefix>com.example.graphql</packagePrefix>
  </configuration>
  <executions>
    <execution>
      <goals>
        <goal>compile</goal>
      </goals>
    </execution>
  </executions>
</plugin>
API version

An API version represents a set of types and operations defined by a GraphQL schema. A version identifier must be a valid Java identifier and not a Java keyword.

You can choose the version identifier to be anything within these restrictions. In this example, the version identifier follows a convention encoding a year, month, and day.

Given a version identifying an API version, and the packagePrefix property configured in the Maven plugin, the framework derives these names by convention.

Concept Value Description

Resources directory

src/main/resources/graphql/version/

You put GraphQL schema definition files for the version in this directory.

Java package

packagePrefix.resolver

You put Java classes defining methods to yield field values in this Java package.

Java package

packagePrefix.version

The compiler generates Java classes for the version under this Java package.

URL path

/graphql/version

The GraphQL server receives requests for the version on this URL path.

This example defines the version v2018_12_31. Create the resources directory src/main/resources/graphql/v2018_12_31/. The compiler generates the code for this version to Java package com.example.graphql.v2018_12_31.

Add this GraphQL schema definition file in the version directory.

By convention, GraphQL schema definition file names end with the extension .graphql.
schema.graphql
type Author {
  id: ID!
  name: String!
}

type Book {
  id: ID!
  title: String!
  price: BigDecimal!
}

type Query {
  author(id: ID!): Author
  book(id: ID!): Book
}

The compiler translates the object types Author and Book to simple Java data classes (also known as a Plain Old Java Object or the acronym POJO), which only hold property values and don’t themselves perform any operations on those properties.

The GraphQL schema defines the root object type Query. The compiler appends the suffix Resolver to this root object type name to derive the Java class name QueryResolver. The compiler assumes there is a Java class named QueryResolver having methods and method parameters corresponding to the Query field and input value definitions. It generates code that invokes these methods.

As an application developer, you must implement the QueryResolver class (and the classes it collaborates with to implement its operations). By convention, this class is in the Java package named packagePrefix.resolver.

QueryResolver.java
package com.example.graphql.resolver;

import com.example.graphql.v2018_12_31.type.Author; (1)
import com.example.graphql.v2018_12_31.type.Book;
import com.example.repository.AuthorRepository;
import com.example.repository.BookRepository;
import org.springframework.stereotype.Component;

@Component
public class QueryResolver {

  @Autowired
  private AuthorRepository authorRepository;

  @Autowired
  private BookRepository bookRepository;

  public Author author(String id) { (2)
    return authorRepository.findById(id);
  }

  public Book book(String id) { (3)
    return bookRepository.findById(id);
  }
}
1 The compiler generated the simple Java data class Author from the GraphQL object type Author.
2 The compiler translated this Java method signature from the field author of the GraphQL root object type Query.
3 The compiler translated this Java method signature from the field book of the GraphQL root object type Query.

Run the application. In GraphQL Playground, connect to http://localhost:8080/graphql/v2018_12_31 to send a GraphQL query to the server.

Mutation

Add a mutation to the GraphQL schema.

schema.graphql
type Author {
  id: ID!
  name: String!
}

type Book {
  id: ID!
  title: String!
  price: BigDecimal!
}

type BookInput { (1)
  title: String
  price: BigDecimal
}

type Mutation { (2)
  createBook(bookInput: BookInput!): Book!
}

type Query {
  author(id: ID!): Author
  book(id: ID!): Book
}
1 Add input type BookInput.
2 Add root object type Mutation.

The compiler assumes there is a Java class named MutationResolver having methods and method parameters corresponding to the Mutation field and input value definitions. It generates code that invokes these methods.

As an application developer, you must implement the MutationResolver class.

MutationResolver.java
package com.example.graphql.resolver;

import com.example.graphql.v2018_12_31.type.Book;
import com.example.graphql.v2018_12_31.type.BookInput; (1)
import com.example.repository.BookRepository;
import org.springframework.stereotype.Component;

@Component
public class MutationResolver {

  @Autowired
  private BookRepository bookRepository;

  public Book createBook(BookInput bookInput) { (2)
    return bookRepository.createBook(bookInput.getTitle(), bookInput.getPrice());
  }
}
1 The compiler generated the simple Java data class BookInput from the GraphQL input type BookInput.
2 The compiler translated this Java method signature from the field createBook of the GraphQL root object type Mutation.

Field

In the GraphQL conceptual model, a field is a function which yields a value. This GraphQL server implementation calls these functions resolvers. The framework implements two ways to yield a value. If a field does not have any arguments, then the framework reads a similarly-named property of a simple Java data object. If a field has one or more arguments, then the framework invokes a method of a Java class, passing the arguments to the method parameters.

Suppose the GraphQL object type Author defines a field books which is intended to provide all books written by the author.

type Author {
  id: ID!
  name: String!
  books: [Book]!
}

The framework will try to read the property books of the simple Java data class Author. To invoke a method of a Java class instead, add a custom directive to the field.

type Author {
  id: ID!
  name: String!
  books: [Book]!
      @argument(name = "authorId", value = "((Author) environment.getSource()).getId()"
}

The @argument directive causes the framework to invoke a method of a Java class, and adds an additional argument to the invocation. The argument value is a Java language expression. In the example expression, environment is an instance of DataFetchingEnvironment.

Add the method to be invoked to the QueryResolver class.

  public List<Book> books(String authorId) {
    return bookRepository.findByAuthorId(authorId);
  }

Modularize GraphQL Schema

As the GraphQL schema grows more complex, you will want to organize the types and operations into multiple schema definition files. The framework merges multiple schema definition files in a version directory into a single GraphQL schema.

As you add operations to the GraphQL root object types, the number of methods you need to maintain in the MutationResolver class and QueryResolver class may become unwieldly. Instead of making these two classes responsible for all your business logic, you can organize the methods into other Java resolver classes.

Delete the schema.graphql file. Other files will replace it.

Define the GraphQL root object type Mutation with no fields. You’re going to extend this type, and type extensions are only allowed on already defined types.

Mutation.graphql
type Mutation {
}

Define the GraphQL root object type Query with no fields.

Query.graphql
type Query {
}

Extend GraphQL root object type Query with author operations. A custom directive ties the fields defined in the object type extension to methods of Java class AuthorResolver. As an application developer, you must implement the AuthorResolver class.

Author.graphql
type Author {
  id: ID!
  name: String!
}

extend type Query @resolve(class: "AuthorResolver") {
  author(id: ID!): Author
}

Similarly extend Mutation and Query with book operations, and tie them to methods of Java class BookResolver.

Book.graphql
type Book {
  id: ID!
  title: String!
  price: BigDecimal!
}

type BookInput {
  title: String
  price: BigDecimal
}

extend type Mutation @resolve(class: "BookResolver") {
  createBook(bookInput: BookInput!): Book!
}

extend type Query @resolve(class: "BookResolver") {
  book(id: ID!): Book
}

API Versioning

Ideally, you want your server to implement a single API version which stays backward compatible. In general, these guidelines help you avoid making breaking changes to your API:

  • Only add new fields.

  • Never delete or alter existing fields.

There may come a time when an incompatible change is required. At that time, create a new API version implementing the incompatible change. The server handles requests to old and new API versions. Resolver classes only handle requests in the newest API version. The framework transforms requests and responses for older API versions into representations the resolver classes can handle.

API versions are sorted from oldest to newest by comparing the version identifier. Numbers in versions are compared numerically. For example, version v2 is older than v11, but they would be sorted in the opposite direction if compared lexicographically.

Object Type Change

For example, let’s add a new API version, v2019_01_01, which introduces an incompatible change. The new API version moves the field price from object type Book to a nested object. Clients will send requests to the new API version at URL path /graphql/v2019_01_01.

Copy resources directory src/main/resources/graphql/v2018_12_31/ to src/main/resources/graphql/v2019_01_01/. Change the Book definition in the new API version.

v2019_01_01/Book.graphql
type Offer {
  price: BigDecimal! (1)
}

type Book {
  id: ID!
  title: String!
  offer: Offer!
}
1 In the previous API version, price is a field of object type Book.

Add a Java class describing the API change and how to transform an object type from the new API version to a representation acceptable to a client of the previous API version.

BookPriceChange.java
package com.example.graphql.v2019_01_01; (1)

import com.example.graphql.v2019_01_01.type.Book;
import com.github.pukkaone.grapid.core.apichange.ObjectTypeChange;
import org.springframework.stereotype.Component;

@Component
public class BookPriceChange
    extends ObjectTypeChange<Book, com.example.graphql.v2018_12_31.type.Book> {

  public BookPriceChange() {
    super("In object type Book, field price moved to field of nested object offer.");
  }

  @Override
  public void downgrade(Book source, com.example.graphql.v2018_12_31.type.Book target) {
    target.setPrice(source.getOffer().getPrice());
    target.removeField("offer");
  }
}
1 By convention, the Java package corresponds to the API version introducing the change.

You must also change the resolver classes to use the Java classes generated from new API version.

Input Type Change

Change the input type BookInput by moving the field price to a nested input.

v2019_01_01/Book.graphql
type OfferInput {
  price: BigDecimal! (1)
}

type BookInput {
  title: String
  offer: OfferInput
}
1 In the previous API version, price is a field of input type BookInput.

Add a Java class describing the API change and how to transform an input type from the previous API version to the new API version.

BookInputPriceChange.java
package com.example.graphql.v2019_01_01;

import com.example.graphql.v2019_01_01.type.BookInput;
import com.example.graphql.v2019_01_01.type.OfferInput;
import com.github.pukkaone.grapid.core.apichange.InputTypeChange;
import org.springframework.stereotype.Component;

@Component
public class BookInputPriceChange
    extends InputTypeChange<com.example.graphql.v2018_12_31.type.BookInput, BookInput> {

  public BookInputPriceChange() {
    super("In input type BookInput, field price moved to field of nested input offer.");
  }

  @Override
  public void upgrade(com.example.graphql.v2018_12_31.type.BookInput source, BookInput target) {
    OfferInput offer = new OfferInput();
    offer.setPrice(source.getPrice());

    target.setOffer(offer);
    target.removeField("price");
  }
}

Enum Type Change

Suppose the previous API version defines an enum type.

enum Meal {
  BREAKFAST
  LUNCH
  DINNER
}

The new API version adds an enum value.

enum Meal {
  BREAKFAST
  SECOND_BREAKFAST (1)
  LUNCH
  DINNER
}
1 The new API version adds this enum value.

Old clients will not understand the new enum value, so transform the new enum value to an enum value acceptable to old clients.

MealSecondBreakfastChange.java
package com.example.graphql.v2019_01_01;

import com.example.graphql.v2019_01_01.type.Meal;
import com.github.pukkaone.grapid.core.apichange.EnumTypeChange;

public class MealSecondBreakfastChange
    extends EnumTypeChange<com.example.graphql.v2018_12_31.type.Meal, Meal> {

  public MealSecondBreakfastChange() {
    super("Added enum value SECOND_BREAKFAST to enum type Meal.");
  }

  @Override
  public String downgrade(String enumValueName) {
    return enumValueName.equals(Meal.SECOND_BREAKFAST.name())
        ? Meal.BREAKFAST.name() : enumValueName;
  }
}

Side Effect

Sometimes a new API version introduces a change in the application’s behavior. The application must execute different logic depending on the API version of the request being processed. The application can use the RequestVersion object to check the API version of the current request.

Inject a RequestVersion instance and the change instance.

@Autowired
private RequestVersion requestVersion;

@Autowired
private MealSecondBreakfastChange mealSecondBreakfastChange;

The application code checks if the change is active for the current request being processed.

if (requestVersion.isActive(mealSecondBreakfastChange)) {