Shahul Basha
From the archive · 2020

REST vs GraphQL: API Development with Spring Boot

Where REST over- and under-fetches, how GraphQL's single endpoint and schema address it, and a small Spring Boot movie API with queries and mutations.

  • 8 min read
  • GraphQL, REST, Spring Boot

Archive note I wrote this in 2020 on my old blog. It moved here in 2026 with the code and diagrams redone; library versions and APIs may have changed since. Original post.

Contents

In this article we’ll build a basic backend API with Spring Boot and GraphQL, and see how it differs from the REST APIs we’re used to building.

GraphQL has been hyped as the next big thing, a revolutionary alternative to REST. It has real advantages, but also limitations, so it’s not all doom and gloom for REST.

REST APIs

REST (Representational State Transfer) is an architectural style for building web services. Endpoints give access to resources, and the data they return (including links) leads the client to more endpoints for the rest of what it needs.

In this example we have related data: customers and their transactions. Three endpoints expose it to the client:

A typical REST API: one endpoint per resource, and one round trip per endpoint.

Limitations of REST

  • Over-fetching. A REST endpoint returns the whole resource, including data you don’t need. You might want only a customer’s name and email, but you get the entire customer object, which makes the payload bigger.
  • Multiple requests and endpoints. With related data, an API often needs several endpoints, and a client that needs all of it has to make several requests. That’s under-fetching.
  • Maintenance and versioning. When the API changes, you version it under a new path, and clients have to adapt to the new response shape. You end up supporting the old API and the new one side by side.

Enter GraphQL

GraphQL is a query language for APIs, developed by Facebook in 2012 and open-sourced in 2015. Facebook needed a robust, fast way to handle a huge number of requests, especially from mobile devices and in regions with slow connections.

Its main features:

  • Declarative data fetching. The client asks for exactly the data it needs: no more over-fetching or under-fetching.
  • A single endpoint. The client always calls the same endpoint, describing the data it wants in the request.
  • Flexibility. Even as the schema changes over the years, you rarely need to version it. You add new fields and deprecate old ones, and each client keeps asking for only what it uses.
One endpoint. Each request body says exactly which fields to return.

There’s only one endpoint, and the request body carries a GraphQL query. The query language looks a bit like JSON, but it isn’t. The query changes from request to request, and only the fields you ask for come back.

The server publishes the fields a client can ask for in advance, as a contract, a bit like a WSDL file for SOAP. In GraphQL, that contract is the schema, written in the Schema Definition Language (SDL).

The GraphQL schema

From here on we’ll explore GraphQL’s concepts while building a simple API with Spring Boot.

The schema is the most important part of the API: it decides which fields and operations clients can use.

We’ll build a movie API that lists movies with their reviews, and lets clients add reviews. Here’s the schema:

schema.graphqlsGraphQL
type Query {
  movies: [Movie]
  movieById(id: ID!): Movie
}

type Mutation {
  createMovie(name: String!, reviewComment: String!): [Movie]
  addReview(id: ID!, reviewComment: String!): Movie
}

type Movie {
  id: ID!
  name: String!
  reviews: [Review]
}

type Review {
  reviewId: ID!
  reviewComment: String
}

Queries and mutations

The schema has four types: Query, Mutation, Movie and Review.

  • Query lists the operations clients can use to read data from the server.
  • Mutation lists the operations that create, update or delete data.

In REST, the HTTP method (GET, POST, PUT, DELETE) says what kind of operation you want. In GraphQL, requests are usually POSTs to the same endpoint, and the request body says which query or mutation to run.

In Query, movies returns a list of movies (the square brackets mean a list), and movieById(id) returns the movie with the given id. The exclamation mark (!) means a value is required.

In Mutation, createMovie takes a movie name and a first review, and returns the list of all movies including the new one. addReview adds a review comment to an existing movie, given its id.

The other two types, Movie and Review, describe the objects clients can get back and the fields they can ask for. Each needs a matching POJO class in the project.

Setting up Spring Boot

Create a simple Spring Boot starter project and add these Maven dependencies for GraphQL:

pom.xmlXML
<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-spring-boot-starter</artifactId>
    <version>5.0.2</version>
</dependency>
<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java-tools</artifactId>
    <version>5.2.4</version>
</dependency>
<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphiql-spring-boot-starter</artifactId>
    <version>5.0.2</version>
</dependency>

Put the schema file in src/main/resources/graphql. It needs the .graphqls extension to be picked up. Then create the Movie and Review POJOs.

Spring Boot now looks for resolver classes that implement the queries and mutations in the schema.

The query resolver

MovieQueryResolver.javaJava
@Component
public class MovieQueryResolver implements GraphQLQueryResolver {

    @Autowired
    MovieService movieService;

    public List<Movie> getMovies() {
        return movieService.getMovies();
    }

    public Movie getMovieById(int id) {
        return movieService.getMovieById(id);
    }
}

The resolver implements GraphQLQueryResolver so it’s recognised as a query resolver. It needs a method for each field in the schema’s Query type, with a matching name and return type. A get prefix is allowed: getMovies() resolves the movies field.

The mutation resolver

MovieMutationResolver.javaJava
@Component
public class MovieMutationResolver implements GraphQLMutationResolver {

    @Autowired
    MovieService movieService;

    // Create a movie
    public List<Movie> createMovie(String name, String reviewComment) {
        Movie movie = new Movie(movieService.getMovies().size(), name,
                new ArrayList<>(Arrays.asList(new Review(1, reviewComment))));
        movieService.getMovies().add(movie);
        return movieService.getMovies();
    }

    // Add a new review to an existing movie
    public Movie addReview(int id, String reviewComment) {
        Movie movie = movieService.getMovieById(id);
        movie.getReviews().add(new Review(movie.getReviews().size(), reviewComment));
        return movie;
    }
}

Likewise, the mutation resolver implements GraphQLMutationResolver and has a method for each field in the Mutation type.

The ids here come from list sizes, which is fine for a demo but clashes quickly: with three movies, a new one gets id 3, which Dora already has. A real database would generate ids.

The service

The resolvers call a service, which fakes a database fetch to keep things simple:

MovieService.javaJava
@Service
public class MovieService {

    private List<Movie> movieList;

    // Simulate a database fetch
    @PostConstruct
    private void init() {
        movieList = new ArrayList<>();
        Review review = new Review(1, "Greatest Movie of All Time!!");
        Review review1 = new Review(2, "The most sick ending..");
        Movie movie = new Movie(1, "The Usual Suspects", new ArrayList<>(Arrays.asList(review, review1)));

        Review review3 = new Review(1, "Poor in every aspect!!");
        Review review4 = new Review(2, "Very Average Movie. Waste of Time..");
        Movie movie2 = new Movie(2, "Artemis Fowl", new ArrayList<>(Arrays.asList(review3, review4)));

        Review review5 = new Review(1, "Fun and Adventurous!!");
        Review review6 = new Review(2, "A good Family movie..");
        Movie movie3 = new Movie(3, "Dora and the Lost City of Gold", new ArrayList<>(Arrays.asList(review5, review6)));
        movieList.add(movie);
        movieList.add(movie2);
        movieList.add(movie3);
    }

    public List<Movie> getMovies() {
        return movieList;
    }

    public Movie getMovieById(int id) {
        return movieList.stream().filter((movie) -> movie.getId() == id).findAny().orElse(null);
    }
}

The POJOs

Both POJOs must have every field in the schema, with the same names and compatible types. Constructors, getters and setters are left out here:

Movie.javaJava
public class Movie {

    private int id;
    private String name;
    private List<Review> reviews;
}
Review.javaJava
public class Review {

    private int reviewId;
    private String reviewComment;
}

We’re all set. Start the application and open localhost:8080/graphiql. That’s a playground where we can test the endpoint with queries and mutations.

The GraphiQL playground

First, all movies with their ids, names and reviews:

QueryGraphQL
query {
  movies {
    id
    name
    reviews {
      reviewId
      reviewComment
    }
  }
}
ResponseJSON
{
  "data": {
    "movies": [
      {
        "id": "1",
        "name": "The Usual Suspects",
        "reviews": [
          { "reviewId": "1", "reviewComment": "Greatest Movie of All Time!!" },
          { "reviewId": "2", "reviewComment": "The most sick ending.." }
        ]
      },
      {
        "id": "2",
        "name": "Artemis Fowl",
        "reviews": [
          { "reviewId": "1", "reviewComment": "Poor in every aspect!!" },
          { "reviewId": "2", "reviewComment": "Very Average Movie. Waste of Time.." }
        ]
      },
      {
        "id": "3",
        "name": "Dora and the Lost City of Gold",
        "reviews": [
          { "reviewId": "1", "reviewComment": "Fun and Adventurous!!" },
          { "reviewId": "2", "reviewComment": "A good Family movie.." }
        ]
      }
    ]
  }
}

The query follows the shape of the data. One rule to know: if you ask for a field that is itself an object, such as reviews, you have to name at least one of its fields. Ask for reviewId, reviewComment or both, or leave reviews out of the query.

Now the same list with only the movie names and review comments:

QueryGraphQL
query {
  movies {
    name
    reviews {
      reviewComment
    }
  }
}
ResponseJSON
{
  "data": {
    "movies": [
      {
        "name": "The Usual Suspects",
        "reviews": [
          { "reviewComment": "Greatest Movie of All Time!!" },
          { "reviewComment": "The most sick ending.." }
        ]
      },
      {
        "name": "Artemis Fowl",
        "reviews": [
          { "reviewComment": "Poor in every aspect!!" },
          { "reviewComment": "Very Average Movie. Waste of Time.." }
        ]
      },
      {
        "name": "Dora and the Lost City of Gold",
        "reviews": [
          { "reviewComment": "Fun and Adventurous!!" },
          { "reviewComment": "A good Family movie.." }
        ]
      }
    ]
  }
}

That’s how simple it is to ask for just the data you want, without cluttering the payload.

Finally, a mutation that adds a review to the movie with id 3, and returns the movie’s id, name and review comments:

MutationGraphQL
mutation {
  addReview(id: 3, reviewComment: "Decent Watch") {
    id
    name
    reviews {
      reviewComment
    }
  }
}
ResponseJSON
{
  "data": {
    "addReview": {
      "id": "3",
      "name": "Dora and the Lost City of Gold",
      "reviews": [
        { "reviewComment": "Fun and Adventurous!!" },
        { "reviewComment": "A good Family movie.." },
        { "reviewComment": "Decent Watch" }
      ]
    }
  }
}

The response includes the new review.

Summary

  • We wrote a schema with queries, mutations, and the Movie and Review types.
  • We wrote query and mutation resolvers to fetch the data for them.
  • In the GraphiQL playground, we asked for exactly the data we wanted, request by request.

Conclusion

In 2020, GraphQL was still growing: not as widely used or battle-tested as REST, but clearly one for the future, with the community building more analytics and tooling around it. It has a learning curve, and nested queries can be confusing at first. But if you need a fast, flexible API for a large and varied set of clients, GraphQL deserves a serious look.

The whole project is on GitHub: shahulbasha/graphqldemo.

← All writing