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.
A wrapper represents an extra layer of computation that can be applied on top of Caliban’s query handling. Its type is
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:
OverallWrapperto wrap the whole query processing
ParsingWrapperto wrap the query parsing
ValidationWrapperto wrap the query validation
ExecutionWrapperto wrap the query execution
FieldWrapperto wrap each field execution
You can also build wrappers from other wrappers:
CombinedWrapperis 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.
EffectfulWrapperis 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.
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:
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.
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
To attach this wrapper to our API, we use the
@@ operator (or
withWrapper if you prefer) on our
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:
ZIO[R, ValidationError, ExecutionRequest]: the computation verifying that the query is valid, and transforms it into an
ExecutionRequestready to be run. That includes replacing the fragments with the actual fields.
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
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
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:
ParsingWrapperto measure the time spent during parsing
ValidationWrapperto measure the time spent during validation
FieldWrapperto measure the time spent by each field
OverallWrapperto 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
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:
ZQuery[R, Nothing, ResponseValue]: instead of a
ZIO, we receive a
ZQueryhere (we’ve seen what it is in Part 2 of this article). Even if you don’t use
ZQueryin your code, Caliban uses it internally to optimize the execution plan. Most combinators that you find on
ZIOare available on
ZQueryas well so it’s not a big change.
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
We will use
ZQuery#summarized to collect the start and the end time, and inject it inside our state together with some field information.
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
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.