Caliban Client: a type-safe GraphQL Client for Scala and Scala.js

Pierre Ricadat
Feb 26 · 5 min read

In the last weeks, I’ve covered how to use Caliban to write type-safe, boilerplate-free and purely functional GraphQL backends in Scala. Today I will talk about a new feature of the library, and something pretty new in the Scala ecosystem: the ability to write and execute GraphQL queries in a type-safe manner.

A lot of developers working with GraphQL on the client-side end up writing queries with external tools such as Altair or Insomnia, and then copy-pasting the code into their own projects. While those tools are great for testing, this string-based approach is error-prone, especially if the schema changes and you have to update all of your queries without any help from a compiler. On top of that, writing complex queries requires knowing GraphQL concepts such as fragments, aliases and variables.

The concept behind caliban-client is that you don’t need to write GraphQL queries directly: instead you implement them in Scala and rely on the compiler to ensure that the code is valid. The process is as follows:

  1. You run an sbt command to generate some boilerplate code from an existing GraphQL schema.
  2. You write GraphQL queries in plain Scala by calling the generated code and combining fields into a larger selection.
  3. You run your queries using sttp, a great library that allows using a wide range of backends and effect systems.

Caliban will take care of generating the actual GraphQL query from your selection, and will also take care of parsing the result into the expected type.

Let’s have a closer look with an example. We will write a client for the Deutsche Bahn GraphQL API, an API from the German railroad company to get information about trains and stations. I picked this one because it has a reasonable size (not too simple but not huge either) and a proper use of types (not everything can be null).

Code generation

The first thing to do is to get your schema in the GraphQL SDL. Most APIs and tools make it easily available for download. If your backend is developed with Caliban, you can get it by calling GraphQL#render on your API definition. Here’s a gist with the schema of our example API.

We now need to run the sbt plugin that will generate our helper code. For that, we add the caliban-codegen sbt plugin to our project and enable it by calling enablePlugins(CodegenPlugin) in our build.sbt file.

The command to run looks like this:

calibanGenClient <schemaPath> <outputPath> <?scalafmtPath>
  • schemaPath is the path to the file containing the GraphQL schema
  • outputPath is the path to the file that should be created with the generated code
  • scalafmtPath is the path to your ScalaFmt config if you want the generated code to be formatted according to your own rules. It is optional and the plugin will look at a potential .scalafmt.conf file at the root of your project by default

For our example, we run this sbt command:

calibanGenClient bahn.graphql src/main/scala/TrainClient.scala

It will create a file called TrainClient.scala containing a Scala object called TrainClient. Inside, there will be an object for each GraphQL type, containing a function corresponding to each GraphQL field inside this type.

For example, the following GraphQL type:

Will generate code like this:

Let’s see what it means and how to use it.

Writing queries

You can see in the previous code snippet that each generated function returns a type called SelectionBuilder. A SelectionBuilder[Origin, A] represents a selection from a GraphQL type named Origin to a result of type A. For example, aSelectionBuilder[Location, Double] means that we select a Double from a GraphQL Location.

Selections that have the same origin can be combined using the ~ operator:

As a result, you get a new selection with the same origin, but with a tuple as the output.

To create a valid query, we need to start from the root type of our API: Query. Let’s have a look at the generated code for Query.search.

It requires an optional search term and an inner selection from Searchable to any type A. Now we can have a look at the Searchable object and we will see we can use Searchable.stations to get the stations matching our input, but that function requires a selection from Station. From the Station object we see there are functions such as Station.name and Station.hasWifi that return such selections. Let’s combine all of them into one query:

This represents a query that will search for the stations named Berlin Ostbahnhof and return the name of each station and whether it has wifi or not. The result will be a list of tuples (String, Boolean). If you don’t want to return tuples, you can use map and mapN on your SelectionBuilder to turn the output into a case class.

We basically built our query by just looking at types and combining existing functions. Now, how do we run our query?

Running queries

Once we’re happy with our selection, we can turn the SelectionBuilder into an sttp Request by calling SelectionBuilder#toRequest. This function requires the URL of the API and will return an executable request. The result type of the request is the result type of your SelectionBuilder.

Sttp requires an implicit backend to run requests. In this example we will use one called AsyncHttpClientZioBackend, but you can use any backend supported by sttp, whether it uses Akka, Monix, etc. Running the request is done by calling send() and gives us back a ZIO Task of the appropriate type.

As you can see, we never had to deal with the GraphQL protocol: the query was generated for us and the result was parsed as well. Everything was just a few lines of code.

Fragments

Let’s push the example a little bit more and request the next arrivals and the next departures for each station (the typo is from the API 😅).

The way we would avoid the repetition in GraphQL would be by creating a fragment and then use it elsewhere. But since we are using Scala, this is even simpler: we create a simple value for the TrainInStation selection and use that value where we need it.

Similarly, if you request the same field multiple times, Caliban-client will take care of generating aliases and parse back the results. One more thing you don’t need to care about.


We’ve seen how caliban-client allows writing type-safe GraphQL queries with only a few lines of code, ensuring that your query is valid at compile-time and taking care of parsing the result for you. If you’d like to give it a try, you may play with the code from this article, available in this repository.

I just released the very first version of this module, and I am sure there are things that can be improved. Feel free to send me feedback and suggestions on the Caliban GitHub repository. You can also find me on Twitter or in the Caliban Discord channel if you’d like to continue the discussion.

Pierre Ricadat

Written by

Software Architect, Photographer

More From Medium

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade