Skip to Content
Course content

70: Akka HTTP for Building APIs

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 OK status.
  • If the category does not exist, return a 404 Not Found with a message stating "Category not found".
  • Ensure the route is nested under a path("categories") directive.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.