DEV Community

Cover image for Lilo the stitching library for GraphQL
Fırat Küçük
Fırat Küçük

Posted on • Updated on

Lilo the stitching library for GraphQL

The story is a well-known Not Invented Here syndrome among developers. Let me summarize from the beginning. Reverse proxies are good especially if you are using REST API, Even if you have multiple microservices behind the proxy. You can efficiently serve all your REST API via using an API Gateway. You can even make some transformation magic, authorization, aggregation, and plenty of other custom flavors. The sky is the limit.

But when it comes to GraphQL, it's not that easy. Especially if you want to transform a legacy code base without changing thousands of lines in frontend. You have 2 options one of them is GraphQL Federation which is a good practice to go with it. But it combines the methods in namespaces, so you might need some refactoring on frontend as well. The second option is GraphQL stitching.

I chose the stitching option. Then I stuck with Atlassian Braid which is a really handy tool TBH. But it seems deprecated and has not been touched for a while. Especially issues sections, there are plenty of bugs reported or questions from 3 years back. I was also trying to use Atlassian Braid on Spring Cloud Gateway. It was really good at the beginning till we deployed it on production. Because of the complex reactive streams logic on the Gateway. It causes lots of thread starvation or unresponsiveness. That eventually triggered me to implement a new one.

Like most of the developers, my estimation was wrong. I've estimated it as a weekend project then it turns out a complete level. then I ended up with Lilo Stitching library and here we are.

How GraphQL Stitching works

How GraphQL Stitching works

When the Stitching library starts, it fetches the schema from the target schema sources, They might be a remote schema source or an internal source as well. After that, it maps all the GraphQL mutations and queries with respective sources.

For every incoming request, it analyzes the query and redirects the requests to respective schema sources after that it combines all the results and returns an aggregated result as a GraphQL response.

Example usage

Installation

Required dependency should be added to your project if you're using maven then you can add it into your pom.xml file:

<dependencies>
  ...
  <dependency>
    <groupId>io.fria</groupId>
    <artifactId>lilo</artifactId>
    <version>23.11.1</version>
  </dependency>
  ...
</dependencies>
Enter fullscreen mode Exit fullscreen mode

If you're using gradle then you can add the dependency in your build.gradle file.

implementation 'io.fria:lilo:23.11.1'
Enter fullscreen mode Exit fullscreen mode

Creating the source applications

You can use lilo with various libraries and web frameworks but I'll try to show a basic usage with spring boot. We need to create two different service projects for exhibiting lilo's skills. Thanks to GraphQL spring project we have now the ability to create GraphQL controller via using annotations just like the REST API.

Controller for Service Application1:

@Controller
public class GreetingController {

  @QueryMapping
  public @NonNull String greeting1() {
    return "Hello world";
  }
}
Enter fullscreen mode Exit fullscreen mode

Then we are going to create a similar controller for our second application:

@Controller
public class GreetingController {

  @QueryMapping
  public @NonNull String greeting2() {
    return "Hello world";
  }
}
Enter fullscreen mode Exit fullscreen mode

We also need to add GraphQL schema definition into resources/graphql directory. For Application1:

type Query {
    greeting1: String
}
Enter fullscreen mode Exit fullscreen mode

same for the application 2:

type Query {
    greeting2: String
}
Enter fullscreen mode Exit fullscreen mode

The package hierarchy should be something like this for both applications:

├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── example
        │           └── stitching_sample
        │               └── server1
        │                   ├── BssServer1Application.java
        │                   └── GreetingController.java
        │                       
        └── resources
            ├── application.properties
            └── graphql
                └── schema.graphqls
Enter fullscreen mode Exit fullscreen mode

It might be wise to assign different port numbers in your application.properties or application.yml using server.port. We will be using 8081 for application 1 and 8082 for application 2.

Creating the gateway

We will be using lilo inside a GraphQL REST controller but you can use it via a filter or you can use it with different web frameworks as well.

@Controller
public class LiloController {

  private static final String SERVER1_NAME = "SERVER1";
  private static final String SERVER1_BASE_URL = "http://localhost:8081";
  private static final String SERVER2_NAME = "SERVER2";
  private static final String SERVER2_BASE_URL = "http://localhost:8082";

  private final Lilo lilo;

  public LiloController() {

    this.lilo =
        Lilo.builder()
            .addSource(
                RemoteSchemaSource.create(
                    SERVER1_NAME,
                    new IntrospectionRetrieverImpl(SERVER1_BASE_URL),
                    new QueryRetrieverImpl(SERVER1_BASE_URL)))
            .addSource(
                RemoteSchemaSource.create(
                    SERVER2_NAME,
                    new IntrospectionRetrieverImpl(SERVER2_BASE_URL),
                    new QueryRetrieverImpl(SERVER2_BASE_URL)))
            .build();
  }

  @ResponseBody
  @PostMapping("/graphql")
  public @NonNull Map<String, Object> stitch(@RequestBody final @NonNull GraphQLRequest request) {
    return this.lilo.stitch(request.toExecutionInput()).toSpecification();
  }
}
Enter fullscreen mode Exit fullscreen mode

When the controller starts, it will create a combined GraphQL schema using the remote sources and it will execute incoming requests using lilo.

We also need additional Retriever definitions. Those classes retrieve the schema or query results. IntrospectionRetriever retrieves the remote schema and QueryRetriever is used for every single request for communicating and executing a single query or a mutation on the remote site.

class IntrospectionRetrieverImpl implements SyncIntrospectionRetriever {

  private final String schemaUrl;
  private final RestTemplate restTemplate;

  IntrospectionRetrieverImpl(final @NonNull String schemaUrl) {
    this.schemaUrl = schemaUrl + "/graphql";
    this.restTemplate =
        new RestTemplateBuilder()
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .build();
  }

  @Override
  public @NonNull String get(
      final @NonNull LiloContext liloContext,
      final @NonNull SchemaSource schemaSource,
      final @NonNull String query,
      final @Nullable Object localContext) {

    return Objects.requireNonNull(
        this.restTemplate.postForObject(this.schemaUrl, query, String.class));
  }
}
Enter fullscreen mode Exit fullscreen mode

and we also need to define our QueryRetriever

class QueryRetrieverImpl implements SyncQueryRetriever {

  private final String schemaUrl;
  private final RestTemplate restTemplate;

  QueryRetrieverImpl(final @NonNull String schemaUrl) {
    this.schemaUrl = schemaUrl + "/graphql";
    this.restTemplate =
        new RestTemplateBuilder() //
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) //
            .build(); //
  }

  @Override
  public @NonNull String get(
      final @NonNull LiloContext liloContext,
      final @NonNull SchemaSource schemaSource,
      final @NonNull GraphQLQuery graphQLQuery,
      final @Nullable Object localContext) {

    return Objects.requireNonNull(
        this.restTemplate.postForObject(this.schemaUrl, graphQLQuery.getQuery(), String.class));
  }
}
Enter fullscreen mode Exit fullscreen mode

now we are ready we can run all 3 applications and see the results.

curl -X POST \
    -H 'content-type: application/json' \
    -d '{"query":"{\ngreeting1\ngreeting2\n}","variables":null}' \
    http://localhost:8080/graphql
Enter fullscreen mode Exit fullscreen mode

greeting1 will be executed on application1 and greeting2 will be executed on application2 we will get the results in a combined GraphQL response.

You can take a look at the sample implementation code on the official Lilo repo. Check out the sample

Top comments (0)