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:
- You run an sbt command to generate some boilerplate code from an existing GraphQL schema.
- You write GraphQL queries in plain Scala by calling the generated code and combining fields into a larger selection.
- 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).
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>
schemaPathis the path to the file containing the GraphQL schema
outputPathis the path to the file that should be created with the generated code
scalafmtPathis 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.conffile 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
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.
You can see in the previous code snippet that each generated function returns a type called
SelectionBuilder[Origin, A] represents a selection from a GraphQL type named
Origin to a result of type
A. For example, a
SelectionBuilder[Location, Double] means that we select a
Double from a GraphQL
Selections that have the same origin can be combined using the
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
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.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
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?
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
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.
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.