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
70: Akka HTTP for Building APIs
I've always found that the best way to wrap your head around Akka HTTP is to stop thinking about it as a "web framework" in the Spring or Play sense, and start thinking about it as a set of directives that filter a request. It's more like a pipeline: the request flows in, and if it matches a certain condition, it gets "caught" and handled. If not, it just keeps flowing down to the next possible route.
The simplest possible listener
Let's start with something that barely works. I want to build a small API for a book inventory system. First, I'll just try to get a server to respond to anything. I'll set up the basic boilerplate to bind a route to a port.
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.model.StatusCodes
object BookApi extends App {
implicit val system = ActorSystem("book-system")
implicit val executionContext = system.dispatcher
val route =
complete(StatusCodes.OK -> "The server is alive!")
Http().newServerAt("localhost", 8080).bind(route)
}
If I run this and hit localhost:8080/books or localhost:8080/random, I get "The server is alive!". That's a start, but it's useless. It doesn't matter what the URL is; the server just shouts the same thing back. I need it to actually care about the path.
Mapping paths to logic
Now I want to target a specific endpoint: /books. In Akka HTTP, we use the path directive. I'll wrap my completion logic inside it. I'm thinking about a simple map to simulate a database for now—keeping it lightweight so we can focus on the HTTP layer.
val inventory = Map("123" -> "The Scala Handbook", "456" -> "Akka in Action")
val route =
path("books") {
complete(StatusCodes.OK -> "You've reached the book list!")
}
That works for /books, but if I try to go to /books/123, I'll get a 404. Why? Because the path("books") directive looks for an exact match. It doesn't automatically assume that anything starting with "/books" belongs here. To handle a specific book ID, I need to use a path variable. I'll use Segment, which captures a single part of the URI.
val route =
path("books" / Segment) { isbn =>
val book = inventory.get(isbn)
complete(StatusCodes.OK -> s"Book found: ${book.getOrElse("Unknown")}")
}
Wait, this is where I usually trip up. If I run this, /books/123 works, but /books (the root) now returns a 404. I've replaced the exact match with a dynamic one. To support both, I need to group them. This is where the concat or simply listing routes in a sequence comes in.
Dealing with the 'Not Found' void
Looking at the code above, there's a bug in my logic. If I search for an ISBN that doesn't exist, like /books/999, the server returns a 200 OK with the text "Book found: Unknown". That's bad API design. If the resource isn't there, the HTTP status code should reflect that.
I'll adjust the logic to check the Option returned by the map. If it's empty, I'll send a StatusCodes.NotFound.
val route =
path("books") {
get {
path "" {
complete(StatusCodes.OK -> "Welcome to the Inventory API")
} ~
path(Segment) { isbn =>
inventory.get(isbn) match {
case Some(title) => complete(StatusCodes.OK -> title)
case None => complete(StatusCodes.NotFound -> s"Book $isbn not found")
}
}
}
}
Notice the ~ operator. That's the "magic" in Akka HTTP. It essentially means "try this route, and if it doesn't match, try the next one." I also wrapped everything in a get directive. I realized that if I sent a POST request to /books, the previous version would have still responded with "Welcome...". Since this is a read-only lookup, I should explicitly restrict this to GET requests.
Plugging it into the system
One last thing. Writing the route is one thing, but handling the server lifecycle is another. If I just call .bind(route), the program might exit immediately because bind returns a Future. I need to ensure the server stays up. In a real app, I'd use a proper lifecycle manager, but for our exploration, I'll just hold the reference to the binding.
The final structure looks like a tree. The request hits the server, filters through the path("books") branch, then the get filter, and finally either hits the empty path or the segment path. It's a very clean way to visualize how a request is routed through your application logic.
📋 Practical Task
Building a Book Category Filter
You are tasked with extending the Book Inventory API. Instead of looking up books by a specific ISBN, you need to implement a category-based lookup.
Requirements:
- Create a data source (a
Map[String, List[String]]) where the key is a category (e.g., "functional", "concurrent") and the value is a list of book titles. - Create a route that responds to
GET /categories/{categoryName}. - If the category exists, return the list of books as a comma-separated string with a
200 OKstatus. - If the category does not exist, return a
404 Not Foundwith a message stating "Category not found". - Ensure the route is nested under a
path("categories")directive.
There are no comments for now.