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:
- Part 1: How to turn a simple API into GraphQL
- Part 2: Nested effects and query optimization
- Part 3: Extend your backend capabilities with wrappers
- Bonus: Caliban Client: a type-safe GraphQL Client for Scala and Scala.js
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 findPug
and randomPugPicture
, and another case class Mutations
for the 2 other endpoints addPug
and editPugPicture
.
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.
Provide resolvers
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 (String
, List
, 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 Schema
for URL
.
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 ArgBuilder
for URL
.
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 api
object.
Serving requests
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.
If you need more detail or have any question, you may consult the Caliban documentation and also join the #caliban channel on Discord where I’m usually active.
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!