Scala
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Scala
-
Section 4: Functional Scala
-
Section 5: Collections in Depth
-
Section 6: Type System
-
Section 7: Concurrency and Ecosystem
-
Section 8: Practical Projects
-
Section 9: Interview Practice
-
Section 10: Data Structures and Algorithms in Scala
-
Section 11: More Practice Exercises
-
Section 12: Advanced Functional Patterns
-
Section 13: More Ecosystem
-
Section 14: Scala Collections Library Deep Dive
-
Section 15: Scala Standard Library Deep Dive
-
Section 16: Akka Ecosystem Deep Dive
-
Section 17: Cats and Cats Effect Deep Dive
-
Section 18: Play Framework Deep Dive
-
Section 19: Apache Spark with Scala Deep Dive
-
Section 20: Scala Build Tools Deep Dive
-
Section 21: Scala 3 Specific Features
-
90: Union and Intersection Types
-
Section 22: Scala Testing Deep Dive
-
Section 23: Functional Domain Modeling
-
Section 24: More Data Structures and Algorithms in Scala
-
Section 25: Scala for Data Engineering
-
Section 26: More Practical Projects
-
Section 27: More Interview and Review
-
Section 28: ZIO Ecosystem Deep Dive
-
Section 29: Scala for Machine Learning
-
Section 30: Scala Microservices Architecture
-
Section 31: Scala Type System Deep Dive
-
Section 32: More Practice and Drills
-
Section 33: Scala Performance Deep Dive
-
Section 34: Scala Ecosystem Tooling
-
Section 35: Scala for Reactive Systems
-
Section 36: More Real-World Case Studies
-
Section 37: Scala for Financial Systems
-
Section 38: Scala GraphQL and gRPC
-
Section 39: More Final Projects
-
Section 40: More Interview and Final Review
-
Section 41: Scala for Streaming Data
-
Section 42: Scala Security Practices
-
Section 43: More Language Deep Dive
-
Section 44: Scala Command-Line Tools
-
Section 45: Scala Documentation and Style
-
Section 46: Scala Dependency Management
-
Section 47: More Practical Backend Patterns
-
Section 48: Scala for Event-Driven Architecture
-
Section 49: More Practice Drills Round 2
-
Section 50: Scala Compiler Deep Dive
-
Section 51: Scala for Web Frontends
-
Section 52: More Data Engineering Practice
-
Section 53: Scala Observability
-
Section 54: More Advanced Practice Projects
-
Section 55: Scala for Legacy Java Integration
-
Section 56: More Testing Practice
-
Section 57: Final Mastery Review
-
Section 58: Scala History and Ecosystem Context
-
Section 59: More Concurrency Patterns
-
Section 60: Scala for Configuration Management
-
Section 61: More Domain Modeling Practice
156: Building a GraphQL Server with Caliban
By now, you're probably used to REST, where you have one endpoint per resource. But GraphQL is a different beast. In the Scala ecosystem, Caliban is the gold standard because it's "code-first"—meaning you define your Scala types, and Caliban derives the GraphQL schema for you. No more jumping back and forth between a .graphqls file and your source code.
How do I actually map my Scala cases to a GraphQL schema?
In Caliban, your schema is essentially a reflection of your Scala types. You start by defining your domain models as case classes and then create a RootResolver. This resolver acts as the entry point for your queries, mutations, and subscriptions.
Let's say we're building a backend for a boutique coffee shop. We have Coffee beans and Orders. Here is how I'd set up the basic structure:
case class Coffee(id: String, name: String, roast: String, price: Double)
case class Order(id: String, coffeeId: String, quantity: Int)
// A mock database
val coffees = Map("1" -> Coffee("1", "Ethiopian Yirgacheffe", "Light", 18.0),
"2" -> Coffee("2", "Sumatra Mandheling", "Dark", 16.0))
val orders = List(Order("101", "1", 2), Order("102", "2", 1))
// The RootResolver defines what the client can ask for
val rootResolver = RootResolver(
Query.root(
// Define a field 'coffee' that takes an ID and returns a Coffee
Field("coffee", ZIO.attempt(coffees.getOption(_).getOrElse(throw new Exception("Not found"))), "Fetch coffee by ID"),
// Define a field 'allCoffees' that returns a list
Field("allCoffees", ZIO.attempt(coffees.values.toList), "List all available coffees")
)
)
val api = GraphQL.schema(rootResolver)
Notice that I'm using ZIO here. Caliban is built on ZIO, which is a huge advantage. It means your resolvers are asynchronous and type-safe by default, which is exactly what you want when you're hitting a database or an external API.
What if I need to fetch related data, like getting the Coffee details for an Order?
This is where a lot of people get stuck. If you just put a coffeeId in your Order class, the client has to make two requests: one for the order and another for the coffee. That's just REST with extra steps. We want the client to be able to query { order { coffee { name } } }.
To do this, you don't put the Coffee object directly inside the Order case class. Instead, you define a "resolver" for that specific field. I prefer this approach because it prevents you from over-fetching data if the client doesn't actually ask for the coffee details.
case class Order(id: String, coffeeId: String, quantity: Int)
// We extend the Order type in the GraphQL schema
val orderResolver = Order.field("coffee", (order: Order) =>
ZIO.attempt(coffees.get(order.coffeeId)).map(_.getOrElse(throw new Exception("Missing coffee")))
)
// Now we apply this resolver to our RootResolver
val rootResolver = RootResolver(
Query.root(
Field("orders", ZIO.attempt(orders), "Get all orders")
),
// This tells Caliban: "Whenever you encounter an Order, use this logic for its fields"
Order.resolvers(orderResolver)
)
One quick tip: Be careful with the "N+1 problem" here. If you fetch 100 orders and then fetch the coffee for each one individually, you've just hit your database 101 times. In a real production app, I'd recommend using Caliban's DataLoader to batch these requests into a single query.
How do I actually serve this schema over HTTP?
Defining the schema is the easy part; making it reachable is where the plumbing comes in. Caliban doesn't force a specific HTTP server on you, but it provides an interpreter that converts a GraphQL request into a ZIO effect.
If you're using caliban-http with something like ZIO Http or Http4s, it looks something like this:
import caliban.execution.GraphQLInterpreter
import caliban.pli.GraphiQL
val interpreter = GraphQLInterpreter(api)
// This creates a handler that takes a request and returns a response
val handler = interpreter.handler
// I also highly recommend adding GraphiQL in development.
// It's that interactive browser IDE that lets you test queries.
val graphiQLHandler = GraphiQL.handler
I usually set up two routes: one for the actual API /graphql and one for the IDE /graphiql. It makes debugging your resolvers ten times faster than using curl or Postman.
📋 Practical Task
Implement a "Total Price" Calculated Field for Coffee Orders
You have the existing Coffee Shop system. Your task is to extend the Order resolver to include a calculated field called totalPrice.
- Modify the
Orderresolver so that when a user queriestotalPrice, the server calculatesOrder.quantity * Coffee.price. - You will need to look up the
Coffeeobject using thecoffeeIdfrom theOrderand then perform the multiplication. - Ensure the field returns a
Double. - Test your implementation by running a query that requests both the
quantityand thetotalPricefor a specific order.
There are no comments for now.