Skip to main content
Version: ZIO 2.x

Tutorial: How to Build a GraphQL Web Service

Having GraphQL APIs enables the clients the ability to query the exact data they need. This powerful feature makes GraphQL more flexible than RESTful APIs.

Instead of having endpoints for our resources, the GraphQL API only provides a set of types and fields in terms of schemas. The client can ask for this schema, and that will help the client to know what kind of data they can expect from the server, and finally, the client can use the schema to build its queries.

GraphQL supports three types of operations: queries, mutations, and subscriptions. In this tutorial, we are going to learn the foundation of GraphQL using queries. Once we learned that, we can easily write two other types of operations.

The corresponding source code for this tutorial is available on GitHub. If you haven't read the ZIO Quickstart: Building GraphQL Web Service yet, we recommend you to read it first and download and run the source code, before reading this tutorial.


In this tutorial, we will build a GraphQL API using Caliban, and in order to serve it, we will use the ZIO HTTP library. So let's install the necessary dependencies by putting the following lines in the build.sbt file:

libraryDependencies ++= Seq(
"dev.zio" %% "zio" % "2.0.0",
"com.github.ghostdogpr" %% "caliban" % "2.0.0",
"com.github.ghostdogpr" %% "caliban-zio-http" % "2.0.0",
"io.d11" %% "zhttp" % "2.0.0-RC10"

Now, we are ready to jump into the next section.

Creating a GraphQL Schema

When working with GraphQL, we need to define a schema, which defines what kind of data with which types we can query. In GraphQL, schemas are defined in their type system. For example, assume we have a data type called Employee, which had defined in scala as the following:

case class Employee(name: String, role: Role)

sealed trait Role
object Role {
case object SoftwareDeveloper extends Role
case object SiteReliabilityEngineer extends Role
case object DevOps extends Role

It has two fields, name, and role, and the role field is a sealed trait, which means that it can only be one of the three values, SoftwareDeveloper, SiteReliabilityEngineer, or DevOps.

If we want to define such a data type in GraphQL, we have something like this:

type Employee {
name: String!
role: Role!

enum Role {

After defining the schema for our data types, the next step is to define queries that can be performed on the data. For example, if we want to query all the employees with a specific role, we can do that like this:

type Queries {
employees(role: Role!): [Employee!]!

Similarly, if we want to query an employee by its name, we can define that like this:

type Queries {
employee(name: String!): Employee

Fortunately, we are not required to define the schema in GraphQL manually. Instead, we can use the Caliban library which has a built-in facility to generate the schema from our data types defined in Scala:

case class EmployeesArgs(role: Role)
case class EmployeeArgs(name: String)

case class Queries(
employees: EmployeesArgs => List[Employee],
employee: EmployeeArgs => Option[Employee]

Running the GraphQL Server

After defining all the queries, in order to serve the GraphQL API, we need to perform the following steps:

  1. Create a GraphQLInterpreter instance, which is a wrapper around the GraphQL API. It allows us to add some middleware around the query execution.
  2. Create an HttpApp instance from the GraphQLInterpreter instance. We can do this by using the ZHttpAdapter.makeHttpService defined in the caliban-zio-http module.
  3. Serve the resulting HttpApp instance using the Server.start method of the ZIO HTTP module.
import caliban.GraphQL.graphQL
import caliban.{RootResolver, ZHttpAdapter}
import zhttp.http._
import zhttp.service.Server
import zio.ZIOAppDefault

import scala.language.postfixOps

object MainApp extends ZIOAppDefault {

private val employees = List(
Employee("Alex", Role.DevOps),
Employee("Maria", Role.SoftwareDeveloper),
Employee("James", Role.SiteReliabilityEngineer),
Employee("Peter", Role.SoftwareDeveloper),
Employee("Julia", Role.SiteReliabilityEngineer),
Employee("Roberta", Role.DevOps)

override def run =
args => employees.filter(e => args.role == e.role),
args => employees.find(e => ==
).interpreter.flatMap(interpreter =>
port = 8088,
http = Http.collectHttp { case _ -> !! / "api" / "graphql" =>


Effectful Queries

In the previous section, we used an in-memory data structure to store the data. But, in real-world applications we usually want to perform some kind of effectful queries to retrieve the data from the database. In such cases, we can use queries that return ZIO values:

case class Queries(
- employees: EmployeesArgs => List[Employee],
+ employees: EmployeesArgs => ZIO[UserRepo, Throwable, List[Employee]],
- employee: EmployeeArgs => Option[Employee]
_ employee: EmployeeArgs => ZIO[UserRepo, Throwable, Option[Employee]]

As we see, each query is a function that takes some arguments and returns a ZIO workflow.

Running the GraphQL Client

In this project, we have defined models of our employees with their names and roles. Then using GraphQL annotations, we asked Caliban to derive the GraphQL schema from these models.

So we can query all the employees that are software developers using the GraphQL query:

employees(role: SoftwareDeveloper){

To run this query, we can use any of the GraphGL clients or use the following curl command:

curl 'http://localhost:8088/api/graphql' --data-binary '{"query":"query{\n employees(role: SoftwareDeveloper){\n name\n role\n}\n}"}'

The response will be as follows:

"data" : {
"employees" : [
"name" : "Maria",
"role" : "SoftwareDeveloper"
"name" : "Peter",
"role" : "SoftwareDeveloper"


In this tutorial, we learned the basic elements of writing GraphQL web services, using the Caliban library. Caliban has great documentation, which can be found here. We can learn more about this project by visiting its website.

All the source code associated with this article is available through the master branch of the ZIO Quickstart: Building GraphQL Web Service.