Skip to Content
Course content

40: Building a Small REST API with a Scala Framework

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

I once worked with a developer named Marc who spent an entire weekend chasing a NullPointerException in a REST endpoint. The API documentation claimed the field asset_id was an integer, but a third-party client was sending it as a string. Because he was using a loosely typed framework, the failure didn't happen at the "edge" of the application where the request arrived; it happened deep in the service layer, three function calls in. It was a nightmare to debug because the stack trace pointed to the logic, not the input.

In Scala, we can avoid this by treating our API boundaries as type-safe contracts. Instead of manually parsing maps or relying on reflection, we use frameworks—like http4s combined with Circe—to ensure that if a request reaches your business logic, it is already validated and typed. If the input is wrong, the framework rejects it immediately with a 400 Bad Request, and your core logic never even has to deal with the mess.

Mapping Endpoints to Business Logic

When building a REST API, the biggest mistake I see is putting too much logic inside the routing definitions. You want your routes to be a thin layer of "plumbing." Let's look at a Digital Asset Registry for a game studio. We need to be able to retrieve a 3D model's metadata by its ID.

case class Asset(id: Int, name: String, polyCount: Int)

val assetRoutes = HttpRoutes.of[IO] {
  case Request[IO](method = Method.GET, path = Path("/assets" / id)) =>
    // We assume assetService is an injected dependency
    assetService.findAsset(id.asInt).flatMap {
      case Some(asset) => Ok(asset.asJson)
      case None        => NotFound()
    }
}

Notice how the path /assets / id treats the ID as a dynamic segment. By calling id.asInt, we're leveraging the framework to handle the type conversion. If the user hits /assets/abc, the framework identifies that "abc" isn't an integer and fails fast. I've found that this declarative style makes the API much easier to audit than a chain of if/else statements checking URL strings.

Bridging the Gap with Circe JSON

A REST API is only as good as its serialization. In the Scala ecosystem, Circe is the gold standard because it doesn't rely on runtime reflection, which can be slow and brittle. Instead, it uses type classes. To make our Asset case class compatible with JSON, we define an encoder and a decoder.

import io.circe.generic.auto._
import io.circe.syntax._

// With auto-derivation, Circe looks at the case class 
// and figures out the JSON mapping at compile time.
val jsonAsset = Asset(1, "Dragon_Model_HighPoly", 50000)
val jsonString = jsonAsset.asJson.noSpaces 
// Result: {"id":1,"name":"Dragon_Model_HighPoly","polyCount":50000}

I usually recommend generic.auto for small projects to keep things moving, but for a production system, I'd suggest using semi-automatic derivation. It's a bit more boilerplate, but it prevents your API from breaking just because you renamed a field in your internal case class.

Launching the Blaze Server

Now that we have routes and JSON handling, we need a server to actually listen for TCP connections. In the http4s world, Blaze is a common choice. The "magic" here is that the server doesn't care about your business logic; it only cares about the HttpRoutes you've defined.

import org.http4s.server.blaze.BlazeServer

object Main extends IOApp {
  def run(args: List[String]): IO[ExitCode] = {
    val routes = assetRoutes.orNotFound
    
    BlazeServer[IO]
      .resource(BlazeServerBuilder[IO].bindHttp(8080, "localhost", routes))
      .use(_ => IO.never) // Keep the server running
      .as(ExitCode.Success)
  }
}

One thing to keep in mind: .orNotFound is a crucial helper. Since HttpRoutes are essentially functions that might return None (meaning "I don't handle this path"), orNotFound ensures that any request that doesn't match your defined routes gets a clean 404 response instead of the server just hanging or crashing.




📋 Practical Task

Build a Game Character Inventory API

Your task is to extend the concepts from this lesson to create a small API for managing game character inventories. You need to implement the following:

  • Define a case class Item with fields: itemId: Int, itemName: String, and powerLevel: Int.
  • Create a POST` endpoint at /inventory that accepts a JSON representation of an Item and returns a 201 Created response if the powerLevel is 100 or greater, and a 400 Bad Request if it is lower.
  • Implement a GET` endpoint at /inventory/status that returns a hardcoded JSON string: {"status": "Online"}.

Ensure you use Circe for the JSON decoding of the Item in the POST request. Your final code should be structured so that it could be passed into a BlazeServerBuilder.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.