GraphQL in Scala with Caliban — Part 1: Turn a simple API into GraphQL
Caliban is a library I created in September 2019 for writing GraphQL backends in Scala in a typesafe, boilerplate-free and purely functional manner. In this series of blog posts, I will demonstrate how it can make your life easier in various scenarios.
I am not going to cover what GraphQL is or how the library is designed, but you may check my talk at Functional Scala 2019 if you’d like to know more. The only prerequisite is some very rough knowledge of ZIO, which is used by Caliban to model effects.
There will be 3 different parts covering the following topics, from the simplest to the most complex:
Let’s say you have a classic REST API and you would like to expose the same functionality using GraphQL. At this stage, you’re not yet concerned with any kind of server optimization: you only want your client code to be able to request only the necessary fields as well as requesting data from multiple endpoints at once.
Let’s take the following
PugService as an example. It’s a basic trait exposing a few features to access or modify a list of pugs and their picture. We can easily imagine each function being called by an endpoint from a REST API.
Now, how do we turn that into GraphQL?
Model your schema with case classes
To expose an API using GraphQL with Caliban, we need to define it using simple case classes where each parameter is equivalent to an endpoint from our REST API.
We need up to 3 case classes:
- one for for Queries: it will contain our read-only endpoints, those we express using GET requests in REST
- one for Mutations: it will contain endpoints for modifying data, those we express using POST, PUT and DELETE requests in REST
- one for Subscriptions: this is not supported by REST, it allows your backend to push events to the client, typically using a WebSocket
For our example, we will create a case class
Queries for our 2 GET endpoints
randomPugPicture, and another case class
Mutations for the 2 other endpoints
This is our API definition: what fields are available, what arguments are needed and what types are returned. As you may have noticed, we use functions
A => B to model GraphQL fields that require arguments, and we use case classes to wrap those arguments and give them a name. I used the suffix
Args to identify them clearly.
We also need a
resolver for each case class (queries, mutations, subscriptions) that we need. A resolver defines how to resolve each field, in other words which function to call when a field is requested. It’s very easy: we just need to create a value for each of our case classes, where each parameter is calling a function from our original business logic.
Now we can finally start using Caliban and turn these resolvers into something useful. First, we group our individual resolvers (queries, mutations, subscriptions) into a
RootResolver, then we pass it to the
graphQL function. This simple call will analyze our data types and values and transform them into a GraphQL API ready to be served.
However, this doesn’t compile. Why?
Custom Schemas for custom types
Caliban knows how to transform all the basic Scala types (
Option, etc.) into GraphQL types. It is also able to automatically transform your own case classes and sealed traits thanks to a library called Magnolia. But if you use any type that doesn’t fit in these 2 categories, you need to tell Caliban how to transform that type into a valid GraphQL type, otherwise it will fail to compile.
This is done by providing an instance of
caliban.schema.Schema for each of these unknown types. In our example, we used
java.net.URL which is not supported natively, so we need to provide a
Schema for it.
The nice thing about
Schema is that you can reuse an existing schema that is similar instead of writing one from scratch. You just need to provide a function to transform from your type to the type you’re reusing. When we have a
URL, we want to render it as a
String, so we can simply start from
Schema.stringSchema and use
contramap to make it a
If we use that custom type as an argument, we also need to provide an instance of
caliban.schema.ArgBuilder for it, to tell Caliban how to parse an argument of that type. Just like
Schema, we can reuse an existing
ArgBuilder to make it easier. Here we start from
ArgBuilder.string and use
flatMap to make it an
And voilà! It compiles! This means Caliban was able to convert each of our Scala types into corresponding GraphQL types. We can verify the GraphQL schema generated by calling
render on our
Then, how do we execute a request? We first need to turn our API into an interpreter by calling
api.interpreter, then we can use the
execute function to perform queries.
execute returns a
GraphQLResponse which can contain
data if the request was successful and
errors if there was any problem during the parsing, the validation or the execution of that request.
That was it! We are now able to execute requests. Altogether, we did nothing really fancy: we had to create a few case classes to model our API, we added support for our custom type
URL and then finally we called Caliban to turn it into a GraphQL interpreter.
Caliban comes with several adapters to serve a given GraphQL interpreter over HTTP using libraries like http4s or Akka HTTP. You can find all the code from this article together with a runnable HTTP server example in this repository.
For some use cases, this is everything you will ever need. But you might want to go deeper and minimize the calls to your backend. In the next part of this series, I will discuss how to handle nested schemas and how to optimize your queries. Stay tuned!