GraphQL in Scala with Caliban — Part 3: Customization

Pierre Ricadat
Feb 10 · 7 min read

This is the third part of an article about Caliban, a library for writing GraphQL backends in Scala in a typesafe, boilerplate-free and purely functional manner. If you haven’t read already, please check Part 1 to understand what this is all about.

We’ve seen that Caliban allows creating GraphQL APIs from Scala types, and that you can support any type that is not automatically handled by creating a custom Schema. But what if you want to customize not just the types of your API, but its behavior?

Here are a few things that you may need:

  • Prevent some queries to be executed because they are too complex (and possibly malicious)
  • Add a timeout to interrupt queries that take too much time to execute, or simply log a warning for slow queries
  • Log every query with its result
  • Collect performance metrics for every field of every query
  • Inject some metadata into the GraphQL response

All of this is possible using a single feature of Caliban called wrappers. Let’s see how to use them.

Wrappers

A wrapper represents an extra layer of computation that can be applied on top of Caliban’s query handling. Its type is Wrapper[-R] where R is the environment that is needed to run the wrapper (Any if not needed).

There are several types of wrappers, depending on the part that you want to wrap:

  • OverallWrapper to wrap the whole query processing
  • ParsingWrapper to wrap the query parsing
  • ValidationWrapper to wrap the query validation
  • ExecutionWrapper to wrap the query execution
  • FieldWrapper to wrap each field execution

You can also build wrappers from other wrappers:

  • CombinedWrapper is created by calling |+| on a wrapper, and will combine two different wrappers. Useful if your custom behavior needs to wrap more than one phase of query handling.
  • EffectfulWrapper is a wrapper that requires an effect to be created. The effect will be run once for each query. This is useful for maintaining a state during query processing, as we’ll see in an example later.

Execution Timeout

Let’s start by writing a simple wrapper to validate that a query doesn’t take too much time to run, and interrupt it when it does.

We will use OverallWrapper to consider the whole query execution time, but we could have used ExecutionWrapper if we only wanted to consider the time spent in our “business” code and exclude the time spent parsing and validating the query.

To create an OverallWrapper[R], we need to provide a function that takes 2 parameters:

  • a URIO[R, GraphQLResponse[CalibanError]]: the computation that will run our query and return a response with either a result or an error. That’s the one we will wrap.
  • a String: our incoming query string. It might be useful to log it in case of timeout.

The function must return a new URIO[R, GraphQLResponse[CalibanError]] that will be executed instead of the previous one.

To implement our timeout, we will call the ZIO method timeout on the received effect. This will give us an Option[GraphQLResponse[CalibanError], which we will then transform into an error when the value is None.

To attach this wrapper to our API, we use the @@ operator (or withWrapper if you prefer) on our GraphQL object.

Query validation

Let’s create another wrapper, this time to verify that the query is not too complex before executing it. To measure the complexity, we will simply use the depth of the query.

The right wrapper for this task is ValidationWrapper[R], because it provides the query already parsed and allows failing before execution. This wrapper needs a function from:

  • a ZIO[R, ValidationError, ExecutionRequest]: the computation verifying that the query is valid, and transforms it into an ExecutionRequest ready to be run. That includes replacing the fragments with the actual fields.
  • a Document: the Scala type that represents our parsed query.

The function must return a new ZIO[R, ValidationError, ExecutionRequest].

To calculate the query depth, we are not going to use the incoming Document, because it might contain fragments that we would have to replace with the actual fields ourselves. Instead, we can flatMap on the result of the computation and work on the ExecutionRequest. This data type contains a parameter called field: Field that contains all the information about the requested fields without any fragments left.

Field has 2 types of children: fields are regular nested fields and conditionalFields are the fields that depend on the object type (used when working with GraphQL union types). To calculate the depth we take the worst case scenario and count all the conditional fields as well.

All we need now is a recursive function from Field to Int that returns the field depth by adding 1 to the depth of the deepest child. Then we can create our wrapper, failing the computation if the calculated depth is higher than the given max depth. You might notice that the recursive method is not stack-safe. We could rely on ZIO trampolining to solve this.

We can attach multiple wrappers to our API by chaining the @@ operators.

Apollo Tracing

Let’s do a more complex one to finish. Apollo Tracing is a GraphQL extension for performance tracing. It measures the time spent by every step of query processing (parsing, validation, and execution of each field) and sticks it into the GraphQL response under the extensions field (see this example). Sounds like a good job for our wrappers!

To tackle this rather large task, we need to build several wrappers and to combine them:

  • a ParsingWrapper to measure the time spent during parsing
  • a ValidationWrapper to measure the time spent during validation
  • a FieldWrapper to measure the time spent by each field
  • an OverallWrapper to measure the overall time spent and also to insert the collected data into the result before returning it to the client

On top of that, we need a way to maintain some state shared between our different wrappers, so that the final OverallWrapper can access the data collected by the other wrappers. For that, we will create a Ref containing that state and we will pass it to each wrapper so that they can update it.

We’ve already seen how to implement a timeout wrapper, so the first wrappers for parsing and validation are pretty trivial: we simply call ZIO#timed to get the time spent by the computation and clock.nanoTime to get the starting time, and update our state stored in Ref.

Below is the implementation for the parsing wrapper. The one for validation is almost identical. The full code for this example with the definition of Tracing can be found here.

A bit more complex is the wrapper for each field. To create a FieldWrapper, we need to provide a function with 2 inputs:

  • a ZQuery[R, Nothing, ResponseValue]: instead of a ZIO, we receive a ZQuery here (we’ve seen what it is in Part 2 of this article). Even if you don’t use ZQuery in your code, Caliban uses it internally to optimize the execution plan. Most combinators that you find on ZIO are available on ZQuery as well so it’s not a big change.
  • a FieldInfo: this data type provides useful information about the field that is currently queried. You can get the field name, its path in the query, its return type as well as the type of its parent.

As usual, the function needs to return a new ZQuery[R, CalibanError, ResponseValue]. A FieldWrapper also takes a boolean called wrapPureValues that determines how to handle pure values (fields that don’t need any effect to run). In some cases, it might be unnecessary to wrap them: for example, they will never time out. Not wrapping them will improve the performance because no overhead is introduced. Apollo Tracing requires data for every field though, so we will use true here.

We will use ZQuery#summarized to collect the start and the end time, and inject it inside our state together with some field information.

The OverallWrapper will be quite similar. The only difference is that it will inject the state containing all the collected data into the GraphQL response. The GraphQLResponse case class has an extensions parameter that allows returning any custom data.

The last part is the most interesting. First, we need to combine these 4 wrappers into a single one, which is easily doable using the |+| operator. Then, we need to handle the Ref object creation. Ref.make returns a UIO[Ref] effect which we can map to pass the Ref to our individual wrappers. Once we combine them, we get a UIO[Wrapper]. That’s when we can use EffectfulWrapper to turn this into a simple Wrapper. The Ref creation effect will be run once for every new request, which is what we want here.

Here’s the code combining all our wrappers into a single one ready to be used:

With this example, we’ve seen how wrappers can be used to support a whole extension of GraphQL in a completely opt-in manner. You could use the same concepts to collect your own metrics and return them in your chosen format, or even send them to some backend. Since wrappers let you execute any kind of effects, the possibilities are endless.

All the wrappers shown in this article are already provided by Caliban.


That’s all I have for this 3-part introduction to Caliban. I hope that was interesting to read and that it made you want to try it by yourself. You may find more details browsing the official documentation. If you have any questions or suggestions regarding the library, please send them to me on github or discord. They are also a few good first issues if you would like to contribute. Happy coding!

A big thanks to Philippe Derome for proofreading and reviewing this whole series.

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