Overview
In this tutorial, we'll implement a sample GraphQL endpoint that makes use of Knot.x Configurable Integrations. It's advised to complete the Getting Started tutorials first. You should also be familiar with fragments. Familiarity with GraphQL is not obligatory (but always welcome).
You'll learn how to:
- use Java and Vert.x GraphQL implementations
- implement them to work seamlessly with Knot.x
- create and use custom Fragments and reuse HTTP Action to fetch data from Google API
GraphQL
GraphQL is a query language, a standard for serving and retrieving data. It's commonly used for communication via HTTP. Unlike REST, GraphQL uses a single endpoint that is serving all data. Clients specify what data they need with a dedicated query language. The server then responds with JSON including only data the client asked for and in the structure he or she wanted.
For example, client could ask for data like this:
{
books(cover_type: "hard") {
available
price
}
}
And get a JSON response:
{
"data": {
"books": [
{
"available": true,
"price": "$12"
},
{
"available": false,
"price": "€15"
}
]
}
}
Maybe we could ask the server for different properties of books or for other products - it doesn't matter. We get what we asked for.
Another advantage of GraphQL is that it's statically typed. The server defines a schema which is a file defining all operations and types the client can request. It's like a model layer.
You can read more at graphql.org.
Java implementation library
Because GraphQL is a specification, it has many implementations for different languages. The most popular Java one is GraphQL Java.
The main idea of this library is a data fetcher. It is an object implementing the DataFetcher
interface with only one method:
T get(DataFetchingEnvironment env)
Let's ignore the env
argument for now. As you might suspect, the role of a data fetcher is to provide some data (return an object, to be precise).
At runtime, you assign data fetchers to types defined in GraphQL schema. Such a fetcher needs to return an object that can be automatically mapped to the schema type. For example, if the schema defines an operation book
returning a type Book
that has a property title
, then we would implement it like that:
- Define a POJO
Book
object
class Book {
private String title;
public Book(String title) {
this.title = title;
}
}
- Define a fetcher
class BookFetcher implements DataFetcher<Book> {
@Override
Book get(DataFetchingEnvironment env) {
return new Book("Reindeers World");
}
}
Note that the example above contains a hardcoded Book instance to keep things simple. Normally the data could be fetched e.g. from the database or 3rd party web service. But as far as GraphQL is concerned, it doesn't matter as long as the fetcher returns a
Book
object.
- Assign the fetcher to operation
//...
RuntimeWiring runtimeWiring = newRuntimeWiring()
.type("QueryType", builder -> builder.dataFetcher("book", new BookFetcher()))
.build()
//then load the schema file and construct GraphQL object
QueryType
is the name of the query we define in a schema.
You'll end up with a graphQL
object (of class GraphQL
). You can then ask this object to process a query, e.g:
{
book {
title
}
}
And you should get the following output:
{
"data": {
"book": {
"title": "Reindeers World"
}
}
}
Quite simple, right?
Vert.x Web GraphQL module
Note that the implementation above focuses on schema and fetchers only. So far it has nothing to do with HTTP. This is where the Vert.x implementation comes in handy. Vert.x provides an out of the box HTTP handler for GraphQL. We just need to provide it with a configured GraphQL
object and it's ready to serve requests through HTTP.
Get the best out of GraphQL and Knot.x
There are two steps to integrate GraphQL and Knot.x. The first is to define a GraphQL handler factory that will return a GraphQLHandler
(the one from the Vert.x implementation). It will allow us to define and configure GraphQL endpoint in the Knot.x configuration.
We could stop at implementing the factory but we wouldn't be using the full power of Knot.x. Knot.x has some great mechanisms for fetching data from 3rd parties, called Configurable Integrations. Therefore, the second step is to create data fetchers that make use of Knot.x integration functionalities.
It's assumed you have the project set up and ready for development. If you have troubles with that check the Getting Started tutorials.
Step 0 - Google Books API, schema and model
Let's stick with books. We'll implement a sample service serving books from the Google Books API. Google Books is a typical REST API. It returns a lot of information. In this tutorial, we'll extract just a title, publisher and a list of authors (Google API returns authors as a list of strings). The response from the Google API looks like this:
{
"items": [
{
//...
"volumeInfo": {
"title": "...",
"publisher": "...",
"authors": ["Author1", "Author2"],
//...
}
}
]
}
Example calls on endpoints that we'll use:
curl https://www.googleapis.com/books/v1/volumes\?q\=Java
curl https://www.googleapis.com/books/v1/volumes/UEdjAgAAQBAJ
Our GraphQL schema should look like this:
schema {
query: QueryType
}
type QueryType {
books(match: String): [Book]
book(id: String): Book
}
type Book {
title: String!
publisher: String!
authors: [String]
}
It defines two operations:
books
: takes a string parameter (search keyword) and returns a list of booksbook
: takes a string parameter (google id) and returns a single book
The schema also defines what a book is. The exclamation point means a field is mandatory (can't be null).
Save it as a books.graphqls
file in your app resources, that is: \modules\books\src\main\resources\
(assuming your module is called "books").
We'll also need to implement a Java model for every type our operations can return. It needs to exactly match the schema. In this case, it's just one class, Book
:
class Book implements GraphQLDataObject {
private String title;
private String publisher;
private List<String> authors;
@Override
public void fromJson(JsonObject json, DataFetchingEnvironment environment) {
JsonObject volumeInfo = json.getJsonObject("volumeInfo");
title = volumeInfo.getString("title");
publisher = volumeInfo.getString("publisher");
authors = new LinkedList<>();
volumeInfo.getJsonArray("authors", new JsonArray()).forEach(object -> authors.add((String) object));
}
}
Note that it's implementing GraphQLDataObject
interface with a fromJson
method. It will come in handy later. It's not a standard interface, you have to define it:
public interface GraphQLDataObject extends Serializable {
void fromJson(JsonObject json, DataFetchingEnvironment environment);
}
Step 1 - The factory
Now, it's time to implement the Handler factory that will setup a GraphQL
object and produce a Vert.x GraphQLHandler
.
public class GraphQLHandlerFactory implements RoutingHandlerFactory {
@Override
public String getName() {
return "graphqlHandler";
}
@Override
public Handler<RoutingContext> create(Vertx vertx, JsonObject config) {
return routingContext -> {
GraphQL graphQL = setupGraphQL(vertx, config, routingContext);
GraphQLHandler
.create(graphQL)
.handle(routingContext);
}
}
//setupGraphQL method implementation
}
It's a standard Knot.x handler factory. It returns a handler that creates a GraphQL
object, uses it to create a Vert.x GraphQLHandler
and lets it handle the request.
GraphQL
object is created the standard way. See graphql-java.com for more explanation on Java GraphQL specific issues.
private GraphQL setupGraphQL(Vertx vertx, JsonObject config, RoutingContext routingContext) {
// Schema file path will be read from knot.x configuration
Reader schema = loadResource(config.getString("schema"));
SchemaParser schemaParser = new SchemaParser();
TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(schema);
RuntimeWiring runtimeWiring = newRuntimeWiring()
.type("QueryType", builder -> builder
// Here we can assign data fetchers to operations from schema
// We'll implement them in Step 2. Normaly it'd look something like:
.dataFetcher("books", new BooksFetcher())
.dataFetcher("book", new SingleBookFetcher())
)
.build();
SchemaGenerator schemaGenerator = new SchemaGenerator();
GraphQLSchema graphQLSchema = schemaGenerator
.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring);
return GraphQL.newGraphQL(graphQLSchema).build();
}
private Reader loadResource(String path) {
return new InputStreamReader(GraphQLHandlerFactory.class.getResourceAsStream("/" + path));
}
Remember about updating services in the META-INF
. With a handler factory, you can now configure a GraphQL endpoint. Add this path to openapi.yml
:
/api/graphql:
post:
operationId: books-gql-operation
responses:
default:
description: Books Graphql API
Note that it needs to be POST because GraphQL reads queries from the requests' bodies. Now add the operation to operations.conf
:
{
operationId = books-gql-operation
handlers = ${config.server.handlers.common.request} [
{
name = graphqlHandler
config = {
schema = "books.graphqls"
}
}
]
}
Note the ${config.server.handlers.common.request}
part. We import some standard configuration because we'll need it later. Now the only thing left are data fetchers and you'll have a working GraphQL endpoint.
Step 2 - Data fetchers and FragmentsEngine
We would like to fetch data from 3rd parties using existing Knot.x mechanisms, not to reinvent the wheel. Ideally we would have a generic data fetcher that can process Knot.x Tasks defined in configuration.
Our fetcher will parse configuration to create Tasks and run them using FragmentsEngine
. It will output a Fragment
that will contain data fetched from Google in its payload. Fragment's payload is just a JsonObject
where actions can store data.
Execute tasks from configuration
Let's start with:
public abstract class TaskDataFetcher<T> implements DataFetcher<CompletableFuture<T>> {
private final Vertx vertx;
private final JsonObject config;
private final RoutingContext routingContext;
private final String taskName;
private final FragmentsEngine engine;
TaskDataFetcher(VertX, JsonObject config, RoutingContext routingContext, String taskName) {
this.vertx = vertx;
this.config = config;
this.routingContext = routingContext;
this.taskName = taskName;
engine = new FragmentsEngine(vertx);
}
@Override
public CompletableFuture<T> get(DataFetchingEnvironment environment) {
FragmentEventContextTaskAware eventContextTaskAware = setupTask(vertx, config, routingContext, environment);
CompletableFuture<T> future = new CompletableFuture<>();
engine
.execute(Collections.singletonList(eventContextTaskAware))
.subscribe(events -> {
// Here we'll get the outcome fragment's payload, parse it to model and complete the future with it
});
return future;
}
//other methods that we'll implement in a moment
}
There's a couple of things to explain here. Firstly, our fetcher is abstract - it will have one abstract method for transforming payload into objects from our model (Book
).
Also, it's generic. That's because it doesn't matter for it what type will it return - it may be Book
, it may be an array Book[]
or it may be anything else.
TaskDataFetcher
implements DataFetcher
(every data fetcher needs to) of type CompletableFuture<T>
. Java implementation of GraphQL allows us to return futures instead of plain objects. If a data fetcher returns a future, GraphQL will asynchronously wait for it to complete. Only when all futures returned by data fetchers complete, the GraphQL will construct a response.
Our abstract fetcher creates an instance of FragmentsEngine. The engine can execute tasks. Task
is a graph of executable nodes. Also, in the get
method we create FragmentEventContextTaskAware
. It's basicaly a task ready to be executed by the engine. Let's define the setupTask
method that will take a configuration and parse it into a task. We'll also need one constant.
private static final String FRAGMENT_TYPE = "graphql-data";
//...
private FragmentEventContextTaskAware setupTask(Vertx vertx, JsonObject config, RoutingContext routingContext, DataFetchingEnvironment env) {
JsonObject fragmentConfig = new JsonObject();
fragmentConfig.put(FRAGMENT_TYPE, taskName);
fragmentConfig.put("gql", new JsonObject(env.getArguments()));
Fragment fragment = new Fragment(FRAGMENT_TYPE, fragmentConfig, "");
RequestContext requestContext = routingContext.get(RequestContext.KEY);
ClientRequest clientRequest = requestContext.getRequestEvent().getClientRequest();
FragmentEvent event = new FragmentEvent(fragment);
FragmentEventContext eventContext = new FragmentEventContext(event, clientRequest);
FragmentsHandlerOptions options = new FragmentsHandlerOptions(config);
ActionProvider proxyProvider = new ActionProvider(options.getAction(), supplyFactories(), vertx.getDelegate());
TaskBuilder taskBuilder = new TaskBuilder(FRAGMENT_TYPE, options.getTasks(), proxyProvider);
Task task = taskBuilder
.build(fragment)
.orElseThrow(() -> new IllegalStateException("No task built"));
return new FragmentEventContextTaskAware(task, eventContext);
}
Let's take a look at it piece by piece:
JsonObject fragmentConfig = new JsonObject();
fragmentConfig.put(FRAGMENT_TYPE, taskName);
fragmentConfig.put("gql", new JsonObject(env.getArguments()));
Fragment fragment = new Fragment(FRAGMENT_TYPE, fragmentConfig, "");
We create a new Fragment that will specify Task to be processed. Fragment's type is used as a Task name. We also put GraphQL environment arguments to the configuration under "gql" key. Environment arguments are just values passed as parameters of an operation. In our case "match" in books(match: String)
and "id" in book(id: String)
. By putting it into a Fragment configuration, we allow Task's nodes (in this case HTTP Actions) to access it. Note that the "gql" key is arbitrary and you can call it anything you want.
For the third argument in the fragment's constructor, we pass an empty string. It's our fragment's body which we're not interested in this tutorial.
RequestContext requestContext = routingContext.get(RequestContext.KEY);
ClientRequest clientRequest = requestContext.getRequestEvent().getClientRequest();
We need to extract ClientRequest
from routingContext
. Remember the ${config.server.handlers.common.request}
part in the operation configuration in operations.conf
? Without it there wouldn't be anything in routingContext
under RequestContext.KEY
.
FragmentEvent event = new FragmentEvent(fragment);
FragmentEventContext eventContext = new FragmentEventContext(event, clientRequest);
Our final Task
will need two things: Task's name and FragmentEventContext
. Here we create the latter. It keeps the client request and our Fragment in the form of FragmentEvent
.
FragmentsHandlerOptions options = new FragmentsHandlerOptions(config);
ActionProvider proxyProvider = new ActionProvider(options.getAction(), supplyFactories(), vertx.getDelegate());
TaskBuilder taskBuilder = new TaskBuilder(FRAGMENT_TYPE, options.getTasks(), proxyProvider);
In order to create a task we need to use a TaskBuilder
. It needs to be supplied with a few things from the configuration. We use FragmentsHandlerOptions
to parse the configuration. We also need a supplier of action factories. We can retrieve it using the standard java ServiceLoader
. We'll take a look at supplyFactories
method in a moment.
Task task = taskBuilder
.build(fragment)
.orElseThrow(() -> new IllegalStateException("No task built"));
return new FragmentEventContextTaskAware(task, eventContext);
Finally we use the task builder to construct a task and return FragmentEventContextTaskAware
that is passed to the fragments engine in get
method. Note that in a real application you would probably want to define a custom exception for when task builder fails. If that happens, it's most likely due to wrong or incomplete configuration.
Now let's quickly implement supplyFactories
method. It simply uses a service loader to retrieve action factories:
private Supplier<Iterator<ActionFactory>> supplyFactories() {
return () -> {
ServiceLoader<ActionFactory> factories = ServiceLoader.load(ActionFactory.class);
return factories.iterator();
};
}
Retrieving payload
Now that we can make our fetcher execute Knot.x tasks from the configuration, we need to retrieve the outcome. As noted before, the data retrieved from the 3rd party (google books api in our case) is stored in the outcome fragment's payload. Therefore our final get
method should look like this:
@Override
public CompletableFuture<T> get(DataFetchingEnvironment environment) {
FragmentEventContextTaskAware eventContextTaskAware = setupTask(vertx, config, routingContext, environment);
CompletableFuture<T> future = new CompletableFuture<>();
engine
.execute(Collections.singletonList(eventContextTaskAware))
.subscribe(events -> {
JsonObject payload = events.get(0).getFragment().getPayload();
JsonObject fetchedData = payload.getJsonObject("fetchedData");
T model = getDataObjectFromJson(fetchedData, environment);
future.complete(model);
});
return future;
}
After the engine is done executing the task we:
- retrieve the fragment and its payload
- get data from payload under "fetchedData" key
- transform this data into our model (in our case it would be
Book
) - complete the future with our model so that GraphQL gets the data
Two things require some explanation. Firstly, we assume that the data we need is under "fetchedData" key. It is not exactly the case but we'll deal with it later. Secondly, we use getDataObjectFromJson
method to transform json into our model. This method is abstract because our TaskDataFetcher
doesn't know how to do it.
abstract T getDataObjectFromJson(JsonObject json, DataFetchingEnvironment environment) throws IllegalAccessException, InstantiationException;
Transforming json into model
Now we need to extend our TaskDataFetcher
with something that can implement getDataObjectFromJson
method. We'll create TaskSingleDataFetcher
that'll transform json into a single model object, for example into Book
.
public class TaskSingleDataFetcher<T extends GraphQLDataObject> extends TaskDataFetcher<T> {
private final Class<T> clazz;
public TaskSingleDataFetcher(String task, Class<T> clazz, Vertx vertx, JsonObject config, RoutingContext routingContext) {
super(vertx, config, routingContext, task);
this.clazz = clazz;
}
@Override
T getDataObjectFromJson(JsonObject json, DataFetchingEnvironment environment) throws IllegalAccessException, InstantiationException {
T dataObject = clazz.newInstance();
dataObject.fromJson(json, environment);
return dataObject;
}
}
It's very simple. It takes a class of some object implementing GraphQLDataObject
to be able to instantiate it. Please note that it must provide also the default constructor. Remember that Book
implements this interface and it has a single method: fromJson
that takes a json and populates the object.
The overriden getDataObjectFromJson
method just creates a new instance of the given class, calls fromJson
on it in order to populate it with data, and then returns it.
The only problem with this fetcher is that it returns single objects only. In our schema we have an operation books(match: String): [Book]
that needs an array of objects. Therefore, we need a second fetcher extending our TaskDataFetcher
that will return an array of objects:
public class TaskArrayDataFetcher<T extends GraphQLDataObject> extends TaskDataFetcher<T[]> {
private Class<T> clazz;
private Function<JsonObject, JsonArray> toArray;
public TaskArrayDataFetcher(String task, Class<T> clazz, Vertx vertx, JsonObject config, RoutingContext routingContext, Function<JsonObject, JsonArray> toArray) {
super(vertx, config, routingContext, task);
this.clazz = clazz;
this.toArray = toArray;
}
@Override
T[] getDataObjectFromJson(JsonObject json, DataFetchingEnvironment environment) throws IllegalAccessException, InstantiationException {
JsonArray jsonArray = toArray.apply(json);
T[] dataArray = (T[]) Array.newInstance(clazz, jsonArray.size());
for (int i = 0; i < jsonArray.size(); i ++) {
T dataObject = clazz.newInstance();
dataObject.fromJson(jsonArray.getJsonObject(i), environment);
dataArray[i] = dataObject;
}
return dataArray;
}
}
It's just a little bit more complicated. It extends TaskDataFetcher<T[]>
instead of TaskDataFetcher<T>
so it can return arrays. In the getDataObjectFromJson
method it creates an array to return and populates it with individual model objects (for example, Book
instances) just the way our previous fetcher did.
Also, there's the toArray
parameter passed to our fetcher. It's a Function
used to transform JsonObject
from payload into a JsonArray
. In our case, google api returns something like:
{
"items": [
{/* book 1 */},
{/* book 2 */ },
//...
]
}
Therefore, we would pass the following function as toArray
:
json -> json.getJsonArray("items")
Using our fetchers
We can now assign our fetchers to operations in runtime wiring builder in GraphQLHandlerFactory
:
RuntimeWiring runtimeWiring = newRuntimeWiring()
.type("QueryType", builder -> builder
.dataFetcher("books", new TaskArrayDataFetcher<>("get-books", Book.class, vertx, config, routingContext, json -> json.getJsonArray("items")))
.dataFetcher("book", new TaskSingleDataFetcher<>("get-book", Book.class, vertx, config, routingContext))
)
.build();
We just assign operation books
to a TaskArrayDataFetcher
that uses task get-books
from configuration and returns an array of Book
objects. Also, we assign operation book
to a TaskSingleDataFetcher
that uses task get-book
and returns a Book
.
Let's set it up in Knot.x configuration. It's best to extract our GraphQL configuration to a separate file. The GraphQL operation configuration in operations.conf
should now look like this:
{
operationId = books-gql-operation
handlers = ${config.server.handlers.common.request} [
{
name = graphqlHandler
config = {include required(classpath("routes/handlers/graphqlHandler.conf"))}
}
]
}
Now we can define GraphQL configuration in a new file, routes/handlers/graphqlHandler.conf
:
schema = "books.graphqls"
tasks {
get-books {
action = getBooks
}
get-book {
action = getBook
}
}
actions {
getBooks {
factory = http
config {
endpointOptions {
path = "/books/v1/volumes?q={config.gql.match}"
domain = www.googleapis.com
port = 443
allowedRequestHeaders = ["Content-Type"]
}
webClientOptions {
ssl = true
}
}
}
getBook {
factory = http
config {
endpointOptions {
path = "/books/v1/volumes/{config.gql.id}"
domain = www.googleapis.com
port = 443
allowedRequestHeaders = ["Content-Type"]
}
webClientOptions {
ssl = true
}
}
}
}
We simply define two tasks (one per each GraphQL operation, we reference their names in fetcher constructors) each with one HTTP action. Those actions call appropriate endpoints:
/books/v1/volumes?q={config.gql.match}
to get books by a given keyword/books/v1/volumes/{config.gql.id}
to get a single book with a given id
Note the {config.gql.xxx}
parts. At runtime its swapped for values from the fragment configuration (remember, we put environment arguments in fragment's config under "gql" key). For example, if we request data like this:
{
book(id: "cool_id") {
title
}
}
Then the http action will swap {config.gql.id}
for "cool_id" and call /books/v1/volumes/cool_id
.
Exposing the data under the right payload key
There is just one last thing we need to do. Every http action saves retrieved data in payload under the action's name and a "_result" subkey. getBook
action will save the data under "getBook._result" key and getBooks
action will save the data under "getBooks._result" key. Remember that our fetcher assumes the data is under "fetchedData" key. Therefore, we need an action that will transfer data in payload from one key to the other. We'll call it "expose-payload-data".
Let's create a simple action factory:
public class ExposePayloadActionFactory implements ActionFactory {
@Override
public String getName() {
return "expose-payload-data";
}
@Override
public Action create(String alias, JsonObject config, Vertx vertx, Action doAction) {
String key = config.getString("key");
String exposeAs = config.getString("exposeAs");
return (fragmentContext, resultHandler) ->
Single.just(fragmentContext.getFragment())
.map(fragment -> {
JsonObject exposedData = fragment.getPayload().getJsonObject(key).getJsonObject("_result");
fragment.appendPayload(exposeAs, exposedData);
return new FragmentResult(fragment, FragmentResult.SUCCESS_TRANSITION)
})
.subscribe(onSuccess -> {
Future<FragmentResult> resultFuture = Future.succeededFuture(onSuccess);
resultFuture.setHandler(resultHandler);
}, onError -> {
Future<FragmentResult> resultFuture = Future.failedFuture(onError);
resultFuture.setHandler(resultHandler);
});
}
}
The action simply reads data from key
key and puts in the exposeAs
key. Remember about updating services in META-INF.
Now we can update our configured tasks to call expose-payload-data
action after succesfully finishing http
action. In case of get-book
task we will expose getBook
as fetchedData
and in case of get-books
task we will expose getBooks
as fetchedData
.
Add two new actions to graphqlHandler.conf
:
exposeInPayload-getBooks {
factory = expose-payload-data
config {
key = getBooks
exposeAs = fetchedData
}
}
exposeInPayload-getBook {
factory = expose-payload-data
config {
key = getBook
exposeAs = fetchedData
}
}
And then setup transitions in our tasks. They should now look like this:
tasks {
get-books {
action = getBooks
onTransitions {
_success {
action = exposeInPayload-getBooks
}
}
}
get-book {
action = getBook
onTransitions {
_success {
action = exposeInPayload-getBook
}
}
}
}
We're done
That's it. You can now run the application. Easiest way of seeing it in action is using postman, but you can use curl or anything else you want.
Let's construct a sample query:
{
book(id: "q5NoDwAAQBAJ") {
title
authors
}
books(match: "java") {
publisher
}
}
We're asking for the title and authors of the book with id "q5NoDwAAQBAJ"
and a list of books about java with only their publishers listed. We'll send our request to http://localhost:8092/api/graphql
If you're using postman you can just choose predefined GraphQL
body type and paste the query there. If you prefer curl:
curl -i -H 'Content-Type: application/json' -X POST -d '{"query": "{book(id: \"q5NoDwAAQBAJ\") {title authors} books(match: \"java\") {publisher}}"}' http://localhost:8092/api/graphql
You should get the following response:
{
"data": {
"book": {
"title": "Learning GraphQL",
"authors": [
"Eve Porcello",
"Alex Banks"
]
},
"books": [
{
"publisher": "Helion"
},
{
"publisher": "Helion"
},
{
"publisher": "Helion"
},
{
"publisher": "\"O'Reilly Media, Inc.\""
},
{
"publisher": "Morgan Kaufmann"
},
//...
]
}
}
Few notes at the end
As you can see Knot.x is highly customizable. Data fetchers that we implemented are quite generic. You can use them to easily create more advanced GraphQL APIs without the need for writing more fetchers. For clarity purposes the code in this tutorial doesn't have advanced error handling and is quite error-prone. It's left as an exercise for you to implement better error handling.
Also, you might have noted that our fromJson
method in GraphQLDataObject
interface has a DataFetchingEnvironment
argument that we didn't use. This is because our example is very simple, but the argument is included in code to show the flexibility of our solution. You can imagine a more complex case where we need to parse JSON to model differently, depending on query parameters. This is where we could use our DataFetchingEnvironment
argument.
Summary of what we actually did
GraphQL and Knot.x represent two seperate layers in our application. GraphQL takes care of gathering the data and outputing it in the desired form. What's important is that it's data-source agnostic. We delegate the responsibility of providing the data to Knot.x. With it comes the idea of Configurable Integrations. We can focus on writing business logic, while error handling is defined in the configuration and therefore can be easily changed. Advanced error-handling systems (e.g. fallbacks, partial failures, timeouts, circuit breakers and other stability patterns) can be quickly set up without any custom logic.
Hopefully, you now have a better understanding of Knot.x, Configurable Integrations and GraphQL.
You can find the complete code of this tutorial in our github repository.