Caliban Documentation

repository·series/3.x·Indexed 21 days ago

https://github.com/ghostdogpr/caliban

A purely functional Scala library for building high-performance GraphQL servers and clients with minimal boilerplate. Caliban derives GraphQL schemas directly from Scala case class hierarchies and provides integrations for various HTTP servers and effect systems, including ZIO, Cats Effect, Monix, http4s, Play, Akka HTTP, Pekko HTTP, ZIO HTTP, and Tapir. It includes support for Apollo Federation subgraph compatibility and compile-time codegen.

Tokens
46.7K
Snippets
145
Records
184
Agent score
75%

What's inside Caliban

  1. Overview of Caliban

    series/3.x

    Caliban is a purely functional library for building GraphQL servers and clients in Scala. It is designed to minimize boilerplate by removing the need to manually define schemas for every type, and it ensures high performance through optimized internals while maintaining a pure and immutable public interface.

    A key architectural feature is the clean separation between schema definition and implementation: the schema is defined and validated at compile time using standard Scala types, while the resolver (RootResolver) is provided as a simple value at runtime.

  2. Overview of Caliban features

    series/3.x

    Caliban is a library designed for high performance, minimal boilerplate, and excellent interoperability. Key features include:

    • High performance: Public interfaces are pure and immutable, while internals are optimized for speed.
    • Minimal boilerplate: The compiler automatically handles schema definitions for your API types, removing the need for manual schema declaration.
    • Excellent interoperability: Provides out-of-the-box support for major HTTP server libraries, effect types, JSON libraries, and more.
  3. Use caliban-tools for introspection and schema comparison

    series/3.x

    The caliban-tools module provides two primary capabilities:

    1. GraphQL Introspection: Use caliban.tools.IntrospectionClient to perform introspection queries against a GraphQL endpoint.
    2. Schema Comparison: Compare GraphQL schemas, whether they are generated by Caliban or retrieved from a remote server.
  4. Build GraphQL queries with SelectionBuilder

    series/3.x

    Once code is generated, every GraphQL type has a corresponding Scala object, and every field has a corresponding function returning a SelectionBuilder[Parent, ResultType].

    Combining Fields

    Use the ~ operator to combine multiple selections. This results in a tuple of the combined types.

    // Returns SelectionBuilder[Character, (String, List[String])]
    val selection = Character.name ~ Character.nicknames

    Mapping to Case Classes

    To avoid working with nested tuples, use .mapN to map selections directly into a case class:

    case class CharacterView(name: String, nickname: List[String], origin: Origin)
    
    val character: SelectionBuilder[Character, CharacterView] =
      (Character.name ~ Character.nicknames ~ Character.origin).mapN(CharacterView)

    Nested Selections

    For fields that return object types, pass a SelectionBuilder as a block to specify which sub-fields to retrieve:

    // Querying a list of characters from the RootQuery
    val query: SelectionBuilder[RootQuery, List[CharacterView]] =
      Query.characters { 
        character 
      }

    Arguments

    If a GraphQL field requires arguments, the generated Scala function will require them as well:

    // Querying characters with a specific origin argument
    val query = Query.characters(Origin.MARS) { character }
    case class CharacterView(name: String, nickname: List[String], origin: Origin)
    
    val character: SelectionBuilder[Character, CharacterView] =
      (Character.name ~ Character.nicknames ~ Character.origin).mapN(CharacterView)
    
    val query: SelectionBuilder[RootQuery, List[CharacterView]] =
      Query.characters(Origin.MARS) {
        character
      }
  5. Make GraphQL fields non-nullable

    series/3.x

    In Caliban, a field is marked as nullable in the schema if the Scala field returns an Option or an effect that can fail.

    To ensure a field is non-nullable:

    • Change the effect to return UIO (a ZIO effect that cannot fail).
    • Use .orDie on your effect to fail the entire query if an error occurs, rather than returning a null value.
  6. How to federate a GraphQL schema

    series/3.x

    Federation allows your graph to become part of a larger graph without brittle schema stitching. You can enable basic federation by wrapping your existing schema with the federated annotation from caliban.federation.v1.

    To support entity resolution, you must:

    1. Annotate types with @GQLKey to define resolvable keys.
    2. Use @GQLExtend and @GQLExternal when extending types defined in other services.
    3. Define an EntityResolver for each type you wish to support.
    4. Pass the resolver(s) to the federated function when wrapping the schema.
    import caliban.federation.v1._
    
    // 1. Basic federation
    val federatedSchema: GraphQL[R] = schema @@ federated
    
    // 2. Entity resolution with keys and resolvers
    @GQLKey("name")
    case class Character(name: String)
    
    val resolver = EntityResolver[CharacterService, CharacterArgs, Character](args => 
      ZQuery.fromZIO(characters.getCharacter(args.name))
    )
    
    // 3. Apply resolvers to the schema
    val federatedSchema = schema @@ federated(resolver)
  7. Interop with contextual effects (Kleisli) in Cats Effect

    series/3.x

    You can share a context between Cats Effect and ZIO using CatsInterop. This is useful when working with contextual effects like Kleisli.

    Use CatsInterop.contextual(dispatcher) to create a CatsInterop.Contextual[Effect, Context] instance. This allows you to convert between RIO[Context, A] and Kleisli[IO, Context, A] using .toEffect and .fromEffect respectively.

    import cats.data.Kleisli
    import cats.effect.IO
    import cats.effect.std.Dispatcher
    import caliban.interop.cats.CatsInterop
    import zio.RIO
    
    trait Context
    type Effect[A] = Kleisli[IO, Context, A]
    
    implicit val dispatcher: Dispatcher[Effect] = ???
    implicit val runtime: Runtime[Context] = ???
    
    val interop: CatsInterop.Contextual[Effect, Context] = CatsInterop.contextual(dispatcher)
    
    val rio: RIO[Context, Int] = ???
    val ce: Kleisli[IO, Context, Int] = ???
    
    val fromRIO: Kleisli[IO, Context, Int] = interop.toEffect(rio)
    val fromCE: RIO[Context, Int] = interop.fromEffect(ce)
  8. How to use the @newtype directive for type-safe IDs

    series/3.x

    The @newtype directive allows you to wrap GraphQL fields (like ID) into statically typed Scala value classes (e.g., FooId) for better backend type safety, without breaking client-side compatibility.

    Usage

    Apply the directive to FIELD_DEFINITION, ARGUMENT_DEFINITION, or INPUT_FIELD_DEFINITION:

    directive @newtype(name : String) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
    
    type Query {
      getFoo(id: ID! @newtype(name: "FooId")): Foo
    }
    
    type Foo {
      id: ID! @newtype(name: "FooId")
    }

    Implementation Details

    • Type Erasure: The generated Scala code uses extends AnyVal. This ensures that at runtime, the types are erased to their underlying primitives, meaning GraphQL queries remain unchanged and performant.
    • Schema Mapping: To make this work, you must provide implicit Schema and ArgBuilder instances in the companion object of your newtype to handle the conversion between the underlying type (e.g., Int or String) and the newtype.
    • Metadata: Caliban uses the @GQLDirective annotation in the generated code to preserve the link between the Scala type and the GraphQL directive.
    // Example of generated newtype with required implicits
    case class FooId(value: Int) extends AnyVal
    object FooId {
      implicit val schema: Schema[Any, FooId] = implicitly[Schema[Any, Int]].contramap(_.value)
      implicit val argBuilder: ArgBuilder[FooId] = implicitly[ArgBuilder[Int]].map(FooId(_))
    }
    
    // Usage in a generated class
    final case class Foo(
      @GQLDirective(Directive("newtype", Map("name" -> StringValue("FooId"))))
      id: FooId
    )
  9. How lazy evaluation works with the @lazy directive

    series/3.x

    To optimize server performance, you can ensure that certain fields are only evaluated if the client explicitly requests them in their query.

    1. Define the directive in your GraphQL schema: directive @lazy on FIELD_DEFINITION
    2. Annotate fields in your schema: myLazyField: String! @lazy

    Resulting Scala Type: Caliban will generate a case class where the annotated field is wrapped in an effect (e.g., zio.UIO). This ensures the logic for that field is only executed upon request.

    directive @lazy on FIELD_DEFINITION
    
    type MyType {
        myLazyField: String! @lazy
        myField: String!
    }
    // Generated output
    case class MyType(myLazyField: zio.UIO[String], myField: String)
  10. Generate GraphQL schemas for case classes and sealed traits

    series/3.x

    Caliban uses the Schema typeclass to transform Scala types into GraphQL types. You can generate Schema instances for your custom types (case classes and sealed traits) using two methods:

    Auto derivation

    Easiest for getting started. Import caliban.schema.Schema.auto._ to automatically generate Schema instances for all case classes and sealed traits found inside your resolver.

    Limitations:

    • Can lead to longer compilation times and high amounts of generated code if types are referenced in multiple places.
    • Error messages for missing nested types may point to the root type instead of the specific missing type.
    • May struggle with highly nested or recursive types.

    Semi-auto derivation

    Recommended for non-trivial schemas to improve compilation and error clarity.

    Scala 2: Use Schema.gen to create an implicit Schema instance.

    Scala 3: Use the derives Schema.SemiAuto syntax on the class, or define a given Schema[Any, MyClass] = Schema.gen.

    // Scala 2
    import caliban.schema.Schema
    case class MyClass(field: String)
    implicit val schemaForMyClass: Schema[Any, MyClass] = Schema.gen
    
    // Scala 3
    import caliban.schema.Schema
    case class MyClass(field: String) derives Schema.SemiAuto
    // OR
    given Schema[Any, MyClass] = Schema.gen
  11. Handle authentication and authorization

    series/3.x

    Authentication and authorization are typically managed using the ZIO environment.

    To require authentication for a field, return an effect that requires an authentication service, such as ZIO[Auth, E, A]. You can then access the Auth service within your resolver to verify permissions. Authentication information can be injected into the environment using middleware in your HTTP server library.