Our journey to route-less HTTP services

Routes at buildo

Claudio Caletti
buildo blog

--

route_66_3

TL;DR, at buildo we don’t write http routes. You can do the same using https://github.com/buildo/wiro.

Kidding aside, we really stopped with routes a few months ago. With the term “routes” I refer to the code that maps http requests to your business logic. Scala, as well as any other language, provides different libraries for that: spray-routing, akka-http, finch and many others. As you might have guessed, we don’t like writing routes so much. Here’s how we ended up writing route-less http services.

What’s wrong with routes?

Since the dawn of time, services at buildo were structured in three layers: routes, controllers and data. The routes were meant to decouple the business logic from the communication. This separation of concerns seemed to be a good idea, even though, in practice, it was never truly useful.

A year ago I started analyzing my sbt compilation logs. I aliased sbt to something like sbt "$1" |tee -a logfile and, a few months later, I had a look at the logs. Here you have my top 3 most common errors:

  • 🥇 Not found
  • 🥈 Type mismatch (the compiler is useful after all)
  • 🥉 Too many arguments for method parameters: (pdm: spray.routing.directives.ParamDefMagnet)pdm.Out

If you ever used spray-routing I bet you’re familiar with the third one. Spray-routing uses a cool and crazy DSL to write routes in Scala, based on the magnet pattern. Despite it being all nice and flexible, it’s also extremely frustrating to debug. It was definitely not increasing my productivity.

Other than my personal issues, the real problem we had with routes was that they represented a large portion of our codebases. The larger the codebase, the more bugs you have to fix. You don’t want to write and maintain unnecessary code.

On top of that, we also noticed that our routes had a one-to-one relationship with our controller methods. They weren’t doing anything clever, we had no good reason to dedicate so much effort to them.

No more mr. nice route

We like interfaces. They’re good for dependency injection and many other nice things. At the time, each of our controllers had an interface that looked like this:

trait CatsApi {
def findCutestCat(): Future[Either[Error, Cat]]
def doSomethingWithTheCat(catId: Int): Future[Either[Error, Unit]]
}

We started playing around with a bunch of libraries:

  • autowire, a set of macros for RPC applications
  • akka-http, the evolution of spray-routing
  • circe, an excellent json library

We decorated the interfaces to enrich them with some routing-related metadata:

@path("cat") //use `cat` as root path
trait CatsApi {
@query //translate this to a GET
def findCutestCat(): Future[Either[Error, Cat]]

@command //translate this to a POST
def doSomethingWithTheCat(catId: Int): Future[Either[Error, Unit]]
}

We twiddled with Scala macros to define a nice syntax to automatically generate akka-http routes from a decorated trait:

//CatsApiImpl is a class implementing CatsApi
val catsRouter = deriveRouter[CatsApi](new CatsApiImpl)

You just need to tell Scala the interface to use to expose the routes:

new HttpRPCServer(
config = Config("localhost", 8080),
routers = List(catsRouter)
)

And there you go! you have an up and running http service that exposes the following routes:

/GET /cat/findCutestCat
/POST /cat/doSomethingWithTheCat -d '{ "catId": 1 }'

🎉 we didn’t have to write routes anymore.

We wrote a library, it’s called Wiro. The core of the library is about 500 lines of code. Most of the work is done by autowire and akka-http. If you want to try it you can find a tutorial here.

Errors Handling

Wiro is not our first attempt to generalize how we write HTTP routes. One of the major pitfalls of the first attempt was errors handling. Our errors looked like this:

abstract sealed trait WebError
object WebError {
case class InvalidParam(param: Symbol, value: String) extends WebError
case class InvalidOperation(desc: String) extends WebError
case object InvalidCredentials extends WebError
case class Forbidden(desc: String) extends WebError
case object NotFound extends WebError
}

This code was written in a library, and the library was shared among different projects.

One day we decided we wanted specific error types and… 💥. Sticking with a single error type throughout a project is a good practice. However, different applications have different needs: it’s hard to generalize errors across different domains.

In Wiro we decided to go with a different approach: we’re using type classes.

trait ToHttpResponse[T] {
def response(t: T): HttpResponse
}

Whatever has an implicit evidence for ToHttpResponse is a proper Wiro error. Something like the following would work:

implicit def error = new ToHttpResponse[Error] {
def response(error: Error) = HttpResponse(
status = StatusCodes.InternalServerError,
entity = "Very Sad"
)
}

Scala is yours IDL

Diving into the web searching for similar libraries, we found out this approach has something to do with RPC. In this context, RPC doesn’t mean you’re running a procedure in a different address space. It doesn’t even imply any sort of location transparency.

The term RPC refers here to an approach to write web services: you can think of it as an alternative to REST. The main difference with REST is that you work with operations rather than resources. This is sometimes called WYGOPIAO: What You GET Or POST Is An Operation.

You write:

POST /cat/doSomethingWithTheCat

instead of:

POST /cat -d '{action: doSomethingWithTheCat}'

In our case, the operations are the methods defined in the interfaces of our controllers. At buildo we use those interfaces to:

  1. Automatically generate an http service (server-side)
  2. Automatically generate the http client and extract the model type definitions used by our web frontends in JavaScript/TypeScript

The first task is exactly what you saw above: Wiro all the way. The second task is done using metarpheus. In case you never heard about it, you might want to read this blog post.

In our applications, Scala traits and models behave as the IDL (Interface Definition Language) for the communication with the web.

No Free Lunch

I can’t tell you whether you should use something like Wiro or not. There’s no such thing as a free lunch.

If your APIs are designed to serve different clients, web and mobile for instance, our approach might lack in flexibility. That’s when you really need to decouple your communication layer from your business logic. In that case, you might want to go with REST. Or try something like GraphQL.

Otherwise, if your APIs are tightly coupled with your client, there’s no need wasting time writing routes.

We are using Wiro in most of our projects, and it’s working out pretty well. Here you have our top 3 good things we noticed since we stopped writing routes:

  • 🥇 We reduced our backend codebases (one of the three layers is gone)
  • 🥈 It’s easier for front-end developers to put hands on the backend
  • 🥉 The names of the routes are more predictable

Finally, I don’t have to deal with Spray’s magnet-pattern-based DSL anymore.

If you want to work in a place where we care about the quality of our development workflow, take a look at https://buildo.io/careers

--

--