# Getting Started
Caliban is a purely functional library for creating GraphQL servers and clients in Scala.
For more details on Caliban Client, see the dedicated section. The rest of this page is about the backend part of the library.
The design principles of Caliban are the following:
- pure interface: errors and effects are returned explicitly (no exceptions thrown), all returned types are referentially transparent (no usage of
Future
). - minimal amount of boilerplate: no need to manually define a schema for every type in your API. Let the compiler do the boring work.
- excellent interoperability: out-of-the-box support for major HTTP server libraries (http4s (opens new window), Akka HTTP (opens new window), Pekko HTTP (opens new window), Play (opens new window), ZIO HTTP (opens new window)), effect types (Future, ZIO (opens new window), Cats Effect (opens new window), Monix (opens new window)), Json libraries (Circe (opens new window), Jsoniter (opens new window), Play Json (opens new window), ZIO Json (opens new window)), various integrations (Apollo Tracing (opens new window), Apollo Federation (opens new window), Tapir (opens new window), etc.) and more.
# Dependencies
To use caliban
, add the following dependency to your build.sbt
file:
"com.github.ghostdogpr" %% "caliban" % "2.3.1"
The following modules are optional:
"com.github.ghostdogpr" %% "caliban-http4s" % "2.3.1" // routes for http4s
"com.github.ghostdogpr" %% "caliban-akka-http" % "2.3.1" // routes for akka-http
"com.github.ghostdogpr" %% "caliban-play" % "2.3.1" // routes for play
"com.github.ghostdogpr" %% "caliban-zio-http" % "2.3.1" // routes for zio-http
"com.github.ghostdogpr" %% "caliban-cats" % "2.3.1" // interop with cats effect
"com.github.ghostdogpr" %% "caliban-monix" % "2.3.1" // interop with monix
"com.github.ghostdogpr" %% "caliban-tapir" % "2.3.1" // interop with tapir
"com.github.ghostdogpr" %% "caliban-federation" % "2.3.1" // interop with apollo federation
"com.github.ghostdogpr" %% "caliban-tracing" % "2.3.1" // interop with zio-telemetry
Support for JSON encoding / decoding of the inputs and responses is enabled by adding one of the following dependencies to your build.sbt
file:
"com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "1.2.11" // Circe
"com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % "1.2.11" // Jsoniter
"com.softwaremill.sttp.tapir" %% "tapir-json-play" % "1.2.11" // Play JSON
"com.softwaremill.sttp.tapir" %% "tapir-json-zio" % "1.2.11" // ZIO JSON
And then later in your code (you only need one!):
import sttp.tapir.json.circe._
import sttp.tapir.json.jsoniter._
import sttp.tapir.json.play._
import sttp.tapir.json.zio._
For more info on the adapters and the JSON implementations, see here.
# A simple example
Creating a GraphQL API with Caliban is as simple as creating a case class. Indeed, the whole GraphQL schema will be derived from a case class structure (its fields and the other types it references), and the resolver is just an instance of that case class.
Let's say we have a class Character
and 2 functions: getCharacters
and getCharacter
:
case class Character(name: String, age: Int)
def getCharacters: List[Character] = Nil
def getCharacter(name: String): Option[Character] = ???
Let's create a case class named Queries
that will represent our API, with 2 fields named and modeled after the functions we want to expose (a record of functions). We then create a value of this class that calls our actual functions. This is our resolver.
// schema
case class CharacterName(name: String)
case class Queries(characters: List[Character],
character: CharacterName => Option[Character])
// resolver
val queries = Queries(getCharacters, args => getCharacter(args.name))
The next step is creating our GraphQL API definition. First, we wrap our query resolver inside a RootResolver
, the root object that contains queries, mutations and subscriptions. Only queries are mandatory.
Then we can call the graphQL
function which will turn our simple resolver value into a GraphQL API definition.
The whole schema will be derived at compile time, meaning that if it compiles, it will be able to serve it.
import caliban._
import caliban.schema.Schema.auto._
import caliban.schema.ArgBuilder.auto._
val api = graphQL(RootResolver(queries))
You can use api.render
to visualize the schema generated, in this case:
type Character {
name: String!
age: Int!
}
type Queries {
characters: [Character!]!
character(name: String!): Character
}
In order to process requests, you need to turn your API into an interpreter, which can be done easily by calling .interpreter
.
An interpreter is a light wrapper around the API definition that allows plugging in some middleware and possibly modifying the environment and error types (see Middleware for more info).
Creating the interpreter may fail with a ValidationError
if some type is found invalid.
for {
interpreter <- api.interpreter
} yield interpreter
Now you can call interpreter.execute
with a given GraphQL query, and you will get an ZIO[R, Nothing, GraphQLResponse[CalibanError]]
as a response, with GraphQLResponse
defined as follows:
case class GraphQLResponse[+E](data: ResponseValue, errors: List[E])
Use ResponseValue#toString
to get the JSON representation of the result.
val query = """
{
characters {
name
}
}"""
for {
interpreter <- api.interpreter
result <- interpreter.execute(query)
_ <- zio.ZIO.debug(result.data.toString)
} yield ()
A CalibanError
can be:
- a
ParsingError
: the query has invalid syntax - a
ValidationError
: the query was parsed but does not match the schema - an
ExecutionError
: an error happened while executing the query
Caliban itself is not tied to any web framework, you are free to expose this function using the protocol and library of your choice.
The caliban-http4s (opens new window) module provides an Http4sAdapter
that exposes an interpreter over HTTP and WebSocket using http4s. There are also similar adapters for Akka HTTP, Pekko HTTP, Play and zio-http.
Read more on the adapters' documentation.
Combining GraphQL APIs
You don't have to define all your root fields into a single case class: you can use smaller case classes and combine GraphQL
objects using the |+|
operator.
val api1 = graphQL(...)
val api2 = graphQL(...)
val api = api1 |+| api2
You can use .rename
to change the names of the generated root types.
# Mutations
Creating mutations is the same as queries, except you pass them as the second argument to RootResolver
:
import zio.Task
case class CharacterArgs(name: String)
case class Mutations(deleteCharacter: CharacterArgs => Task[Boolean])
val mutations = Mutations(_ => ???)
val api = graphQL(RootResolver(queries, mutations))
# Subscriptions
Similarly, subscriptions are passed as the third argument to RootResolver
:
import zio.stream.ZStream
case class Subscriptions(deletedCharacter: ZStream[Any, Nothing, Character])
val subscriptions = Subscriptions(???)
val api = graphQL(RootResolver(queries, mutations, subscriptions))
All the fields of the subscription root case class MUST return ZStream
or ? => ZStream
objects. When a subscription request is received, an output stream of ResponseValue
(a StreamValue
) will be returned wrapped inside an ObjectValue
.
Schemas →