Introduction

Microservices architecture

In a microservices architecture, several loosely coupled, independently deployable services comprise a large software application.

This type of design can provide quite a few benefits, including:

  • ability to release more often and more reliably

  • codebases easier to reason about

  • independence to a particular technology

For example, an online store application could be composed of, among others, an inventory and a rating service:

microservices architecture
Figure 1. Online store application, microservices architecture

The API Gateway pattern

The client side of the application may have to contact several services to display a single screen or page. For instance, to display a product details page, it needs product info from the inventory service and customer reviews from the rating service.

But each network communication comes with a cost. This applies to desktop browsers with DSL or fiber links, but the impact is particularly significant on mobile networks.

This is one important reason why the API Gateway pattern has emerged. In practice, the client communicates with a single service, the gateway, which communicates with the backend services:

api gateway pattern
Figure 2. API Gateway pattern
Note

The API Gateway does not eliminate the coordination required to make sense of the backend data. In fact, it adds a new network hop in the the sequence of service calls.

But it moves most of the interactions inside the datacenter, where network latencies are considerably better.

Very often, the API Gateway provides cross-cutting concerns like security. For example, it could generate a token after authenticating the client and communicate it to the backend services as a proof of identity.

It may also provide its own features, if they are only relevant to the frontend. An e-commerce application, for instance, could manage customers' shopping carts at the API Gateway level.

From a performance standpoint, an event-driven implementation would give better results under load. Indeed, the API Gateway’s workload is dominated by I/O, so having threads blocked waiting is not an optimal usage of resources.

Serving many clients

A lot of applications nowadays must be available from a wide range of clients: browsers, smartphones, tablets, wearables…​ etc. These clients all have different capabilities in terms of display and networking.

Let’s consider the product details use case. For a desktop user, it makes sense to show:

  • the product info (inventory service)

  • the average rating (rating service)

  • users' reviews (rating service)

However, in a smartphone app the reviews would probably be accessible in a separate view because of the screen size and shape.

But if the mobile developer uses the same endpoint as the web developer, the API Gateway wastes:

  • time (latency) waiting for rating service responses, and

  • bandwith sending a lot of unnecessary data.

To overcome this problem, it is possible to create a backend specific to each type of frontend.

backend for frontends
Figure 3. Backend for Frontends pattern

Nevertheless, this design, also known as the Backend for Frontends pattern, has a few drawbacks:

  • each specific API Gateway is another component to maintain

  • a lot of code is duplicated

  • each new feature has to be supported in all gateways before all clients can start using it

GraphQL in a nutshell

What it is

GraphQL is a query and schema definition language for your backend services.

It allows backend developers to describe the data in a language-agnostic fashion:

GraphQL Schema file
type Genre {
  id: Int!
  name: String!
}

type Album {
  id: Int!
  name: String!
  genre: Genre!
  artist: String!
  reviews: [Review!]
}

type Review {
  name: String!
  rating: Int!
  comment: String
}

type Query {
  albums(genre: Int): [Album!]
}

schema {
  query: Query
}

And then frontend developers to request exactly the information they need:

GraphQL query
query ($id: Int!) {
  album(id: $id) {
    id
    name
    genre {
      name
    }
    artist
  }
}

Which, given an id variable, would result in:

GraphQL results
{
  "album": {
    "name": "Revolver",
    "genre": {
      "name": "Pop"
    },
    "artist": "The Beatles"
  }
}

While the GraphQL specification does not prescribe any transport, in practice it’s often used over HTTP and Websockets.

Tip
You will find a GraphQL schema definition and query authoring introduction on https://graphql.org/learn/.

How it works

When a GraphQL server runtime starts, it:

  1. parses the schema file to discover types and fields

  2. binds each field to data fetching functions

graphql server runtime
Figure 4. GraphQL server runtime creation

Then when a request is received, it:

  1. validates the query

  2. invokes each data fetching function needed to produce the result

  3. sends the result to the client

graphql query execution
Figure 5. GraphQL query execution phases

API Gateway engine

GraphQL becomes more and more popular, including as a replacement for RESTful or HTTP/JSON APis.

But it particularly shines when building API Gateways. Why? Let’s consider the product details use case again.

When the desktop client sends a request to the GraphQL runtime, it will ask for product info as well as users' review. And the runtime will execute the corresponding data fetchers:

Desktop client query
query ($id: Int!) {
  album(id: $id) {
    id
    name
    genre {
      name
    }
    artist
    reviews {
      name
      comment
      rating
    }
  }
}

However, the smartphone client will only ask for the product info:

Smartphone client query
query ($id: Int!) {
  album(id: $id) {
    id
    name
    genre {
      name
    }
    artist
  }
}

And the runtime will NOT execute the data fetchers for customers' reviews and, obviously, will not send the unnecessary data.

The MusicStore Application

The MusicStore is an online music shop. You can browse its catalog by genre, read customer reviews, see the list of tracks. When logged-in, you may add albums to your cart, manage your cart items or post your own reviews.

Technically, it is comprised of the following components:

musicstore app
Figure 6. The MusicStore application
Note
In a real-world application, the static content would often be served from a separate component.

Web Client

A Single Page Application implemented with Vue.js and Apollo client.

Inventory Service

The Inventory Service exposes product data over HTTP in JSON format:

  • genres

  • albums (name, artist, genre, …​etc)

  • tracks

For the sake of simplicity, inventory data is loaded on startup from text files and stored into memory.

Rating Service

The Rating Service receives customers' reviews on albums:

  • customer name

  • rating

  • comment

It exposes this data over HTTP in JSON format. It can also compute an average rating for each album.

Again, for simplicity, reviews are stored only in memory.

Building blocks

You will build the API Gateway for the MusicStore using the following technologies.

GraphQL-Java

The GraphQL-Java library is the Java implementation of the GraphQL specification.

To configure it, you must provide at least:

  • a GraphQL schema, either from a definition file or built programmatically

  • data fetchers functions for schema types and fields (runtime wiring)

A GraphQL-Java data fetcher must implement the graphql.schema.DataFetcher interface. In practice, it must have a get method that takes a graphql.schema.DataFetchingEnvironment argument and returns a result:

DataFetcher interface
public interface DataFetcher<T> {
    T get(DataFetchingEnvironment environment) throws Exception;
}

The result can be a CompletionStage if it is provided asynchronously.

The environment argument gives information about the current position in the graph: source type, arguments, fields to be fetched…​ etc

The GraphQL-Java runtime job is to execute queries and it does not provide any transport implementation.

Eclipse Vert.x

Vert.x is a toolkit to write asynchronous and reactive applications on the JVM.

It implements the reactor pattern and uses Netty for the networking layer. It is similar to Node.js but it is capable of scaling across CPU cores instead of using a single thread.

Vert.x does not mandate any programming model but the easiest way to get started is to create a verticle.

A verticle is an entry-point class with a start method:

Vert.x verticle
public class MyVerticle extends AbstractVerticle {

 // Called when verticle is deployed
 public void start() {
 }

 // Optional - called when verticle is undeployed
 public void stop() {
 }

}

The core library provides low-level abstractions to start HTTP servers, work with the filesystem, …​etc.

Vert.x Web GraphQL integrates the GraphQL-Java library so that queries and results can be sent over HTTP and Websockets.

RxJava

Quotinq the RxJava project website:

RxJava – Reactive Extensions for the JVM – a library for composing asynchronous and event-based programs using observable sequences for the Java VM.
— RxJava project

When working with asynchronous APIs, the code can quickly become difficult to understand if you need to execute several sequential operations. It’s even worse when composing results of concurrent operations.

This issue is widely known as callback-hell and RxJava provides operators to deal with it.

Note
CompletableFuture / CompletionStage introduced in JDK 8 helps a lot with asynchronous composition. But RxJava comes with a broader range of operators and is also able to deal with asynchronous data flows.

Let’s take a couple of examples:

Sequential composition: async Postgres query execution and transformation of row results into a Java object
public Single<Cart> findCart(String username) {
  return pool.rxPreparedQuery(FIND_CART, Tuple.of(username))
    .flatMapObservable(Observable::fromIterable)
    .map(CartRepository::rowToCartItem)
    .collectInto(new Cart(), Cart::add);
}
Concurrent composition: combining results of async web client requests into a single Java object
Single<Album> inventoryData = albumsRepository.findById(id, true);
Single<RatingInfo> ratingData = ratingRepository.findRatingAndReviewsByAlbum(id);
return inventoryData.zipWith(ratingData, (a, r) -> {
  a.setRating(r.getRating());
  a.setReviews(r.getReviews());
  return a;
});

By default, Vert.x uses a callback style but it also has an Rxified API.

Getting started

Prerequisites

Java Development Kit

JDK 8 or later must be installed on our machine. If you don’t have it already, you can get one from:

You can use either OpenJDK or Oracle JDK.

Maven

Download Apache Maven from https://maven.apache.org/download.cgi.

Extract the archive contents to a directory of your choice and add it to the PATH.

IDE

It is recommended to use an IDE. It does not matter if it’s IntelliJ, Eclipse or Netbeans.

If you don’t have an IDE, follow these instructions to get started with Eclipse:

  • browse to the Eclipse downloads page

  • select the Eclipse IDE for Java Developers package and download it

  • extract the archive contents to a directory of your choice

  • in the destination directory, execute the Eclipse binary

  • create a workspace

Postgres database

Important
The Postgres database is only required to complete the last step or to run the full solution. If you cannot get it to work on your machine, you can still complete all the other steps.

If you have Docker running on your machine, you can start the Postgres database in a container:

Starting a Postgres Database with Docker
docker run -p 5432:5432 -e POSTGRES_USER=musicstore -e POSTGRES_PASSWORD=musicstore -d postgres
Tip
Linux and Mac users can simply execute the run-postgres.sh script after having imported the code in the next step.

Otherwise:

  • download Postgres from https://www.postgresql.org/download/ and follow the instructions for your machine type

  • create a musicstore database

  • create a musicstore user with password musicstore

  • grant the musicstore user with the permission to create tables on the the musicstore database

As a superuser you could run these queries:

CREATE DATABASE musicstore;
CREATE USER musicstore WITH ENCRYPTED PASSWORD 'musicstore';
GRANT ALL PRIVILEGES ON DATABASE musicstore TO musicstore;

Importing the code

The project code is hosted on GitHub. Open a terminal in the directory of your choice and type:

git clone https://github.com/tsegismont/graphql-api-gateway-workshop.git

Alternatively, you may download the project archive from GitHub and extract the content.

Inside the project directory, run Maven to build the project:

mvn install

Now open the IDE and import the project.

In Eclipse:

  • click on File > Import

  • select Maven > Existing Maven Projects

  • in Root Directory, type the project directory path or select it with the Browse…​ button

  • make sure the root project and all sub-projects checkboxes are ticked and click Finish

In IntelliJ:

  • click on File > New > Project from Existing Sources

  • select the pom.xml file at the root of the project directory path and click OK

  • click Next on the following wizard panels and then Finish

Running backend services

Important
Make sure you have built the project beforehand and started the Postgres database.

Inventory

Open a terminal at the root of the project directory and type:

cd inventory
./run.sh

On Windows, open the file explorer in the inventory directory and execute run.bat.

If the service starts correctly, you should see a line similar to this on the console:

[2019-11-02 21:05:24] [INFO   ] Succeeded in deploying verticle

Rating

Open a terminal at the root of the project directory and type:

cd rating
./run.sh

On Windows, open the file explorer in the rating directory and execute run.bat.

If the service starts correctly, you should see a line similar to this on the console:

[2019-11-02 21:05:24] [INFO   ] Succeeded in deploying verticle

Let’s code!

This lab is divided into several steps incrementally building the API Gateway for the MusicStore application.

Each step can be run separately. To do so, open a terminal in the step directory and type:

mvn clean vertx:run

Then start coding. The step projects all use the Vert.x Maven plugin which automatically redeploys the code when a file changes.

On Windows, open the file explorer in the step directory and execute run.bat.

The files to edit are either in the src/main/java or the src/main/resources directory.

Each step has its solution in the src/solution directory. You may run it with:

mvn clean package -Psolution
java -jar target/step-0.jar -Dvertxweb.environment=dev

On Windows, open the file explorer in the step directory and execute run-solution.bat.

Important
To avoid port conflicts, don’t forget to close the server before you start working on the next step.

A complete solution can be found in the gateway directory.

Step 0: Vert.x Web Router

The Vert.x core library, as explained above, has very low-level abstractions for HTTP servers.

Vert.x Web is a module that builds on top of Vert.x core and provides features commonly found in a web server: static file serving, session management, authentication and authorization and much more.

The basic Vert.x Web construct is the Router. It allows to create routes matching requests by path or HTTP method and delegates execution to handlers.

Router router = Router.router(vertx); // (1)

router.get().handler(StaticHandler.create()); // (2)

router.route().failureHandler(ErrorHandler.create()); // (3)
  1. Router creation for a given Vert.x instance

  2. Any GET request should be handled by the StaticHandler for static file serving

  3. If anything gets wrong for any type of request, invoke the ErrorHandler.

Exercise

In the IDE, open the steps/step-0/src/main/java/workshop/gateway/Step0Server.java file.

Implement the TODOs found in the createRouter method.

Observations

The browser should display the MusicStore welcome page.

Note
The error message Failed to load data…​ is expected as we haven’t implemented the GraphQL runtime yet.

Step 1: The GraphQL-Java runtime

Now that we have a web server, let’s configure the GraphQL runtime.

Firstly, we need a GraphQL schema file.

The schema must contain at least a root Query type and its fields are the entry points to the graph:

type Genre {
  id: Int! # (3)
  name: String! # (4)
  image: String
}

type Query { # (1)
  genres: [Genre!] # (2)
}

schema {
  query: Query
}
  1. root query type

  2. the genres field returns an array of Genre objects

  3. Integer is one of the built-in scalar types

  4. fields are nullable unless marked by !

Secondly, we must configure the runtime to define data fetchers. In GraphQL-Java this is called runtime wiring:

protected RuntimeWiring runtimeWiring() {
  return RuntimeWiring.newRuntimeWiring()
    .type("Query", this::query) // (1)
    .build();
}

private TypeRuntimeWiring.Builder query(TypeRuntimeWiring.Builder builder) {
  return builder
    .dataFetcher("genres", new GenresDataFetcher(genresRepository)) // (2)
    ;
}
  1. Runtime wiring configuration of the root Query type provided by the query method

  2. Define the GenresDataFetcher object for the genres field of the Query type

Note
The workshop.gateway.WorkshopVerticle class exposes a few repositories that implement the data access layer to the inventory service, the rating service, as well as the Postgres database.

The GenresDataFetcher extends workshop.gateway.RxDataFetcher, which is an adpater for the RxJava Single type that GraphQL-Java does not understand:

package workshop.gateway;

import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import hu.akarnokd.rxjava2.interop.SingleInterop;
import io.reactivex.Single;

import java.util.concurrent.CompletionStage;

public interface RxDataFetcher<T> extends DataFetcher<CompletionStage<T>> {

  @Override
  default CompletionStage<T> get(DataFetchingEnvironment environment) throws Exception {
    Single<T> single = rxGet(environment);
    return single==null ? null:single.to(SingleInterop.get());
  }

  Single<T> rxGet(DataFetchingEnvironment env) throws Exception;
}

Lastly, the Vert.x Web router must declare a route for /graphql requests and set the GraphQLHandler:

GraphQL graphQL = setupGraphQLJava("musicstore.graphql");
router.route("/graphql").handler(GraphQLHandler.create(graphQL));

Exercise

In the IDE, open the steps/step-1/src/main/resources/musicstore.graphql file.

Implement the TODOs found in the Genre and Query types.

Open the steps/step-1/src/main/java/workshop/gateway/Step1Server.java file.

Implement the TODOs found in the createRouter and query methods.

Open the steps/step-1/src/main/java/workshop/gateway/GenresDataFetcher.java file.

Implement the TODOs found in the rxGet method.

Observations

The browser should display the list of genres available in the MusicStore.

GraphiQL is an IDE to author GraphQL queries.

In the query panel, type the following query and execute it:

query {
  genres {
    id
  }
}

In the results panel you should see:

{
  "data": {
    "genres": [
      {
        "id": 1
      },
      {
        "id": 2
      },
      {
        "id": 3
      }
    ]
  }
}

Modify the query to fetch other fields (name or image). The result should have exactly the information you required, no more no less.

Tip
In the top right corner of the web page, click the Docks link. GraphiQL will open the schema explorer. This can be useful when working with a schema that you do not own.

Step 2: Fields with arguments

GraphQL types can have fields taking arguments. This can be useful for filtering or paging results.

type Query {
  genres: [Genre!]
  albums(genre: Int): [Album!] # (1)
}
  1. the albums field of the root Query type takes a genre argument of type Int

In the data fetcher implementation, the argument value is retrieved from the DataFetchingEnvironment:

Integer genre = env.getArgument("genre");

Exercise

In the IDE, open the steps/step-2/src/main/resources/musicstore.graphql file.

Implement the TODOs found in the Album and Query types.

Open the steps/step-2/src/main/java/workshop/gateway/Step2Server.java file.

Implement the TODO found in the query method.

Open the steps/step-2/src/main/java/workshop/gateway/AlbumsDataFetcher.java file.

Implement the TODO found in the rxGet method.

Observations

In the query panel, type the following query and execute it:

query($genre:Int) {
  albums(genre: $genre)  {
    id
    name
    genre {
      name
    }
    artist
  }
}

In the results panel you should see all albums because we haven’t provided any value for this nullable argument.

Then open the query variables panel and type:

{"genre": 2}

Execute the query again. Now in the results panel you should see:

{
  "data": {
    "albums": [
      {
        "id": 9,
        "name": "Dark Side Of The Moon",
        "genre": {
          "name": "Rock"
        },
        "artist": "Pink Floyd"
      },
      {
        "id": 10,
        "name": "Appetite For Destruction",
        "genre": {
          "name": "Rock"
        },
        "artist": "Guns N' Roses"
      },
      {
        "id": 11,
        "name": "Back In Black",
        "genre": {
          "name": "Rock"
        },
        "artist": "AC/DC"
      },
      {
        "id": 12,
        "name": "Master Of Puppets",
        "genre": {
          "name": "Rock"
        },
        "artist": "Metallica"
      }
    ]
  }
}

Step 3: Fetching data efficiently

In this step, the schema has been modified in order to get all albums details in a single query:

type Genre {
  id: Int!
  name: String!
  image: String
}

type Track {
  number: Int!
  name: String!
}

type Album {
  id: Int!
  name: String!
  genre: Genre!
  artist: String!
  image: String
  tracks: [Track!]!
  rating: Int
  reviews: [Review!]
}

type Review {
  name: String!
  rating: Int!
  comment: String
}

type Query {
  genres: [Genre!]
  albums(genre: Int): [Album!]
  album(id: Int!): Album
}

schema {
  query: Query
}

Then of course the corresponding data fetcher has been defined in runtime wiring:

package workshop.gateway;

import graphql.GraphQL;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.TypeRuntimeWiring;
import io.vertx.reactivex.ext.web.Router;
import io.vertx.reactivex.ext.web.handler.BodyHandler;
import io.vertx.reactivex.ext.web.handler.ErrorHandler;
import io.vertx.reactivex.ext.web.handler.StaticHandler;
import io.vertx.reactivex.ext.web.handler.graphql.GraphQLHandler;
import io.vertx.reactivex.ext.web.handler.graphql.GraphiQLHandler;

public class Step3Server extends WorkshopVerticle {

  protected Router createRouter() {
    Router router = Router.router(vertx);

    router.route().handler(BodyHandler.create());

    GraphQL graphQL = setupGraphQLJava("musicstore.graphql");
    router.route("/graphql").handler(GraphQLHandler.create(graphQL));
    router.get("/graphiql/*").handler(GraphiQLHandler.create());

    router.get().handler(StaticHandler.create());
    router.get().handler(WorkshopVerticle::rerouteToVueIndex);

    router.route().failureHandler(ErrorHandler.create());

    return router;
  }

  protected RuntimeWiring runtimeWiring() {
    return RuntimeWiring.newRuntimeWiring()
      .type("Query", this::query)
      .build();
  }

  private TypeRuntimeWiring.Builder query(TypeRuntimeWiring.Builder builder) {
    return builder
      .dataFetcher("genres", new GenresDataFetcher(genresRepository))
      .dataFetcher("albums", new AlbumsDataFetcher(albumsRepository))
      .dataFetcher("album", new AlbumDataFetcher(albumsRepository, ratingRepository))
      ;
  }
}

In the data fetcher implementation, two concurrent requests are sent to the inventory and rating services and results get merged, thanks to RxJava’s zip operator:

Single<Album> inventoryData = albumsRepository.findById(id, true); // (1)
Single<RatingInfo> ratingData = ratingRepository.findRatingAndReviewsByAlbum(id); // (2)
return inventoryData.zipWith(ratingData, (a, r) -> { // (3)
  a.setRating(r.getRating());
  a.setReviews(r.getReviews());
  return a;
});
  1. Load album data (including tracks) from inventory service

  2. Load rating and reviews from rating service

  3. Merge rating data into one Album object

Why fetching all this data at once?

Because if a user sends a query for a specific album, chances are high the full set of fields will be needed.

Consequently, it is more efficient to:

  1. retrieve album data with tracks included in a single backend request

  2. retrieve rating data concurrently

Exercise

In the IDE, open the steps/step-3/src/main/java/workshop/gateway/AlbumDataFetcher.java file.

Implement the TODOs found in the rxGet method.

Observations

In the query panel, type the following query and execute it:

{
  album(id: 8) {
    name
    artist
    rating
    reviews {
      name
    }
    tracks {
      number
      name
    }
  }
}

In the results panel you should see:

{
  "data": {
    "album": {
      "name": "Thriller",
      "artist": "Michael Jackson",
      "rating": 0,
      "reviews": [],
      "tracks": [
        {
          "number": 1,
          "name": "Wanna Be Startin' Somethin'"
        },
        {
          "number": 2,
          "name": "Baby Be Mine"
        },
        {
          "number": 3,
          "name": "The Girl Is Mine"
        },
        {
          "number": 4,
          "name": "Thriller"
        },
        {
          "number": 5,
          "name": "Beat It"
        },
        {
          "number": 6,
          "name": "Billie Jean"
        },
        {
          "number": 7,
          "name": "Human Nature"
        },
        {
          "number": 8,
          "name": "P.Y.T. (Pretty Young Thing)"
        },
        {
          "number": 9,
          "name": "The Lady in My Life"
        }
      ]
    }
  }
}

Of course rating data is not available yet. But if you open the inventory service console, you will see a single request for each album GraphQL query:

[2019-11-03 10:12:23] [INFO   ] GET /album/8?withTracks= 200 481 - 1 ms

Step 4: On demand data fetching

In the previous step, we made tracks, rating and reviews data available when users execute the album query.

But what about a user who would like to get some or all of this data from another entry point in the graph?

For example, if you display a list of albums of a single genre, you might want to present each album’s rating on the web page.

To do so, the runtime wiring must define how these Album fields can be fetched:

protected RuntimeWiring runtimeWiring() {
  return RuntimeWiring.newRuntimeWiring()
    .type("Query", this::query)
    .type("Album", this::album) // (1)
    .build();
}

private TypeRuntimeWiring.Builder album(TypeRuntimeWiring.Builder builder) { // (2)
  return builder
    .dataFetcher("tracks", new AlbumTracksDataFetcher(tracksRepository))
    .dataFetcher("rating", new AlbumRatingDataFetcher(ratingRepository))
    .dataFetcher("reviews", new AlbumReviewsDataFetcher(ratingRepository))
    ;
}
  1. Define Album runtime wiring in the album method

  2. Data fetchers definitions for fields that are not always present in the Album object

Let’s take a look at the data fetcher implementation:

package workshop.gateway;

import graphql.schema.DataFetchingEnvironment;
import io.reactivex.Single;
import workshop.model.Album;
import workshop.repository.RatingRepository;

public class AlbumRatingDataFetcher implements RxDataFetcher<Integer> {

  private final RatingRepository ratingRepository;

  public AlbumRatingDataFetcher(RatingRepository ratingRepository) {
    this.ratingRepository = ratingRepository;
  }

  @Override
  public Single<Integer> rxGet(DataFetchingEnvironment env) throws Exception {
    Album album = env.getSource();
    Single<Integer> rating;
    if (album.getRating()!=null) {
      rating = Single.just(album.getRating());
    } else {
      rating = ratingRepository.findRatingByAlbum(album.getId());
    }
    return rating;
  }
}
Important
Notice that the rating field is loaded only if not already present. Indeed, without this protection, we would send two requests to the rating service when we execute the album query.

Exercise

In the IDE, open the steps/step-4/src/main/java/workshop/gateway/Step4Server.java file.

Implement the TODO found in the runtimeWiring method.

Open the steps/step-4/src/main/java/workshop/gateway/AlbumTracksDataFetcher.java file.

Implement the TODOs found in the rxGet method.

Observations

In the query panel, type the following query and execute it:

{
  albums(genre: 2) {
    name
    tracks {
      name
    }
  }
}

In the results panel you should see:

{
  "data": {
    "albums": [
      {
        "name": "Dark Side Of The Moon",
        "tracks": [
          {
            "name": "Speak To Me"
          },
          {
            "name": "Breathe (In The Air)"
          },
          {
            "name": "On The Run"
          },
          {
            "name": "Time"
          },
          {
            "name": "The Great Gig In The Sky"
          },
          {
            "name": "Money"
          },
          {
            "name": "Us And Them"
          },
          {
            "name": "Any Colour You Like"
          },
          {
            "name": "Brain Damage"
          },
          {
            "name": "Eclipse"
          }
        ]
      },
      {
        "name": "Appetite For Destruction",
        "tracks": [
          {
            "name": "Welcome To The Jungle"
          },
          {
            "name": "It's So Easy"
          },
          {
            "name": "Nightrain"
          },
          {
            "name": "Out Ta Get Me"
          },
          {
            "name": "Mr. Brownstone"
          },
          {
            "name": "Paradise City"
          },
          {
            "name": "My Michelle"
          },
          {
            "name": "Think About You"
          },
          {
            "name": "Sweet Child O' Mine"
          },
          {
            "name": "You're Crazy"
          },
          {
            "name": "Anything Goes"
          },
          {
            "name": "Rocket Queen"
          }
        ]
      },
      {
        "name": "Back In Black",
        "tracks": [
          {
            "name": "Hells Bells"
          },
          {
            "name": "Shoot to Thrill"
          },
          {
            "name": "What Do You Do for Money Honey"
          },
          {
            "name": "Givin the Dog a Bone"
          },
          {
            "name": "Let Me Put My Love Into You"
          },
          {
            "name": "Back In Black"
          },
          {
            "name": "You Shook Me All Night Long"
          },
          {
            "name": "Have a Drink on Me"
          },
          {
            "name": "Shake a Leg"
          },
          {
            "name": "Rock and Roll Ain't Noise Pollution"
          }
        ]
      },
      {
        "name": "Master Of Puppets",
        "tracks": [
          {
            "name": "Battery"
          },
          {
            "name": "Master Of Puppets"
          },
          {
            "name": "The Thing That Should Not Be"
          },
          {
            "name": "Welcome Home (Sanitarium)"
          },
          {
            "name": "Disposable Heroes"
          },
          {
            "name": "Leper Messiah"
          },
          {
            "name": "Orion"
          },
          {
            "name": "Damage Inc."
          }
        ]
      }
    ]
  }
}

Then type and execute this albumQuery:

{
  album(id: 1) {
    tracks {
      number
      name
    }
  }
}

In the inventory service console, you should a single request logged:

[2019-11-03 10:12:23] [INFO   ] GET /album/1?withTracks= 200 528 - 1 ms

Browse to http://localhost:8080 and explore the MusicStore catalog.

Step 5: Authentication

It is good if an online store lets you browse its product catalog. But isn’t it better if customers can buy items and write reviews?

Before doing so, a critical piece is missing on the API Gateway: authentication.

In the real-world, authentication for microservices-based applications is often delegated to a separated component.

But here we will simply focus on how:

  1. security can be added to the Vert.x Web server

  2. user info can be retrieved from data fetchers

Vert.x Web provides security handlers that can be added to the router to implement different authentication mechanisms.

Let’s say we want to authenticate users with a form, storing credentials in an htpassword file. When authenticating, the server should store user info in the web session. If the user logs out, the session data must be destroyed eagerly instead of waiting for session timeout.

First, an auth provider must be created:

HtpasswdAuthOptions authOptions = new HtpasswdAuthOptions()
  .setHtpasswdFile("passwordfile")
  .setPlainTextEnabled(true);
HtpasswdAuth authProvider = HtpasswdAuth.create(vertx, authOptions);
Warning
Do not enable plaintext htpassword files in production!

Then a session handler added to the router:

SessionHandler sessionHandler = SessionHandler.create(LocalSessionStore.create(vertx)).setAuthProvider(authProvider);
router.route().handler(sessionHandler);

The session handler will inspect all incoming requests to determine if a session cookie is present and add user info the the RoutingContext.

HTTP POST requests to the /login.html path shall be handled by a form login handler:

FormLoginHandler formLoginHandler = FormLoginHandler.create(authProvider).setDirectLoggedInOKURL("/");
router.post("/login.html").handler(formLoginHandler);

Lastly, HTTP GET requests to the /logout path should trigger session data removal:

router.get("/logout").handler(rc -> {
  rc.clearUser();
  rc.session().destroy();
  rc.response().setStatusCode(307).putHeader(HttpHeaders.LOCATION, "/").end();
});

This is the first time we implement a Vert.x Web handler.

In a nutshell, a Vert.x Web handler is an operation that takes a single RoutingContext argument and interact with the HTTP server request and response.

A handler can terminate the routing process by replying to the response or hand-over to the next handler by calling RoutingContext#next().

Authentication is now set up, but what about retrieving user info in data fetchers?

The DataFetchingEnvironment provides a context object. Since the GraphQL runtime is independent of any transport, the content of this object depends on the GraphQL-Java integration.

Vert.x Web GraphQL, by default, puts the RoutingContext instance in the GraphQL context. Consequently it allows you to retrieve the user info:

RoutingContext routingContext = env.getContext();
User user = routingContext.user();
return user!=null ? user.principal().getString("username"):null;

Exercise

In the IDE, open the steps/step-5/src/main/java/workshop/gateway/Step5Server.java file.

Implement the TODOs found in the createRouter method.

Open the steps/step-5/src/main/java/workshop/gateway/UserUtil.java file.

Implement the TODOs found in the currentUser method.

Observations

Browse to http://localhost:8080 and click on the login menu button located at the top right.

You can either connect with username vladimir or thomas. The password is the username.

The Add to cart button should now be visibile as well as the review input section at the bottom of the page.

Step 6: Mutations

We have seen that a GraphQL schema must contain at least a root Query.

Another entry point to the graph is the Mutation type.

Mutations, in the schema file, look like their queries counterpart, except that:

  • the different semantic is a signal for ther server runtime and client implementations that backend data is going to change

  • they can take an Input argument

Here is how the schema is changed to support customer reviews:


input ReviewInput {
  rating: Int!
  comment: String
}

type ReviewResult {
  rating: Int!
  reviews: [Review!]!
}

type Mutation {
  addReview(albumId: Int!, review: ReviewInput): ReviewResult
}

On the server-side, GraphQL-Java executes queries and mutations differently: while it does not invoke data fetchers for query fields in any particular order, those of mutations are, as per the specification, always invoked serially and in the order defined by the schema.

Otherwise mutation data fetchers are configured exactly in the same way:

protected RuntimeWiring runtimeWiring() {
  return RuntimeWiring.newRuntimeWiring()
    .type("Query", this::query)
    .type("Mutation", this::mutation)
    .type("Album", this::album)
    .build();
}

private TypeRuntimeWiring.Builder mutation(TypeRuntimeWiring.Builder builder) {
  return builder
    .dataFetcher("addReview", new AddReviewDataFetcher(ratingRepository))
    ;
}

In the data fetcher implementation, the input argument is retrieved from the DataFetchingEnvironment:

String currentUser = UserUtil.currentUser(env);
if (currentUser==null) {
  throw new NotLoggedInException();
}
Integer albumId = env.getArgument("albumId");
ReviewInput reviewInput = EnvironmentUtil.getInputArgument(env, "review", ReviewInput.class);
reviewInput.setName(currentUser);
return ratingRepository.addReview(albumId, reviewInput);

Exercise

In the IDE, open the steps/step-6/src/main/resources/musicstore.graphql file.

Implement the TODOs found in the ReviewInput input type.

Open the steps/step-6/src/main/java/workshop/gateway/AddReviewDataFetcher.java file.

Implement the TODOs found in the rxGet method.

Observations

Browse to http://localhost:8080 and click on the login menu button located at the top right.

You can either connect with username vladimir or thomas. The password is the username.

Select an album and scroll down to the bottom of the page. Add a review.

You should see the review result added below the form.

Then scroll up to the top and the average rating should have been updated.

Browse back to the genre page of this album.

The average rating of the album should be present.

Note that, when displaying the genre page, the rating service console shows that the API Gateway sent a request only for the average rating, not for the reviews:

[2019-11-05 10:58:22] [INFO   ] GET /album/12/rating 200 11 - 1 ms

Step 7: On demand data fetching in mutation results

Important
You cannot complete this step if you haven’t setup the Postgres Database successfully.

In the previous step, we added a Mutation type for customer reviews. The addReview mutation returned a ReviewResult that had all the data loaded.

However, since mutation results behave the same as query results, we can also fetch data on demand.

Let’s say we modify the schema to support adding and removing items from a shopping cart:

type CartItem {
  album: Album
  quantity: Int
}

type Cart {
  items: [CartItem!]
}

type Mutation {
  addToCart(albumId: Int!): Cart
  removeFromCart(albumId: Int!): Cart
  addReview(albumId: Int!, review: ReviewInput): ReviewResult
}

We must define a data fetcher for the album field of the CartItem type:

protected RuntimeWiring runtimeWiring() {
  return RuntimeWiring.newRuntimeWiring()
    .type("Query", this::query)
    .type("Mutation", this::mutation)
    .type("Album", this::album)
    .type("CartItem", this::cartItem)
    .build();
}


private TypeRuntimeWiring.Builder cartItem(TypeRuntimeWiring.Builder builder) {
   return builder
    .dataFetcher("album", new CartItemAlbumDataFetcher(albumsRepository))
    ;
}

In the data fetcher implementation, we should return the album data from the inventory service:

CartItem cartItem = env.getSource();
return albumsRepository.findById(cartItem.getAlbumId(), false);
Note
The withTracks param is set to false because it is unlikely that a page dealing with the shopping cart will need the tracks list. It would still be available on demand though.

Exercise

In the IDE, open the steps/step-7/src/main/resources/musicstore.graphql file.

Implement the TODO found in the CartItem input type.

Open the steps/step-7/src/main/java/workshop/gateway/Step7Server.java file.

Implement the TODO found in the cartItem method.

Open the steps/step-7/src/main/java/workshop/gateway/CartItemAlbumDataFetcher.java file.

Implement the TODOs found in the rxGet method.

Observations

Browse to http://localhost:8080 and click on the login menu button located at the top right.

You can either connect with username vladimir or thomas. The password is the username.

Select an album and click the Add to cart button.

You should see a popup with the message Done!.

On the top right of the page, the cart icon should show a badge with the number of items in the cart.

Click on the cart icon.

In the cart page, add or remove items.

Conclusion

Congratulations! You have implemented the API Gateway pattern with GraphQL.

You learnt how to:

  • configure the GraphQL-Java runtime

  • provide a HTTP transport for the queries

  • implement data fetchers for queries and mutations

  • fetch data efficiently

  • protect users with authentication

Want to go further? Consider reading the: