Lessons in Functional API Development from Haskell’s Servant and Http4s

Lessons in Functional API Development from Haskell’s Servant and Http4s

Starting around September, I started work on replicating a small piece of the Raster Foundry backend code in Haskell. Raster Foundry’s API server uses akka-http for routing and doobie for database interaction. I planned to use servant for the REST interface and postgresql-simple for database interaction on the Haskell side. In the middle of this 10% time project, we also migrated our tile server from akka-http to http4s. This blog post summarizes lessons learned and future priorities from my different experiences developing RESTful interfaces in these three different contexts. The goal isn’t to pick a winner, but instead to compare and find what works well in each setting and prioritize future learning.

Routing

Each of the three libraries offers a different way to express what your API looks like. In akka-http, you compose routing directives:

val projectRoutes: Route = handleExceptions(userExceptionHandler) {
  pathEndOrSingleSlash {
    get {
      listProjects
    } ~
      post {
        createProject
      }
  } ~
    pathPrefix(JavaUUID) { projectId =>
      pathEndOrSingleSlash {
        get {
          getProject(projectId)
} ~ { ... }

In http4s, you provide partial functions and pattern match using the http4s dsl:

val routes: AuthedService[User, IO] =
  AuthedService {
    case GET -> Root / UUIDWrapper(projectId) / "layers" / UUIDWrapper(
          layerId) / IntVar(z) / IntVar(x) / IntVar(y) :? BandOverrideQueryParamDecoder(bandOverride) as user =>
      ...

In servant, you turn on the DataKinds and TypeOperators extensions, then describe your API as a type:

type VenueAPI =
  SAS.Auth '[SAS.JWT] Player :> "venues" :> (
  QueryParam "freeNights" [Time.DayOfWeek] :> Get '[JSON] [Venue]
  :<|> Capture "venueID" UUID :> Get '[JSON] (Maybe Venue)
  :<|> Capture "venueID" UUID :> ReqBody '[JSON] Venue :> Put '[JSON] Int64
  :<|> ReqBody '[JSON] Venue :> PostCreated '[JSON] (Maybe Venue)
)

The most interesting part of these differences is what responsibilities they put on the programmer. For example, in akka-http and http4s, the description of your API and its implementation are tightly coupled. If you have a route at /api/hello that responds to GETrequests, you must implement something to respond (even if it just sends back empty Ok responses or throws an exception). In servant, you don’t. Servant separates the concerns of describing how the API behaves (what are the routes, the parameters, etc.) from what happens when someone sends a request to the route. You’ll obviously have to implement something if you want your API type to do any work, but you can even use servant to provide well-typed clients for other people’s APIs.

Authentication

Another difference between akka-http services and http4s and servant services is that authentication is a part of the routing directives in akka-http, while in the other two it’s provided separately.

In servant, you define the authentication strategy in the type, and later provide settings that make evaluating that authentication strategy possible. In http4s, you do even less — you just tell the compiler whether your routes are going to have some kind of authorization and what the return type of that authorization has to be. Both of those strategies allow you to separate your authentication implementation from evaluating your routes, which should facilitate easier testing. In akka-http, our authentication strategy is intrinsically linked to our routes, and untangling it has sounded unpleasant enough that we bailed on API tests and have had an issue open for a year and a half that we never seem to have time for.

In http4s, you specify only:

val routes: AuthedService[User, F] = { ... }

where F is some effect type, and User is what you expect the authentication eventually to produce. Then inside the route you have normal route matchers that end as user to make the types line up.

In servant, authentication is just another composable type component, e.g.

type FooAPI = SAS.Auth '[SAS.JWT] User :> ...

where SAS.Auth is a two parameter type taking a list of authentication strategies, SAS.JWT is an authentication strategy, and User is the type of thing the authentication strategy will return. Enforcement of JWT resolvability also happens at the type level — if you don’t provide a type in the User slot that has a FromJWT instance, your code won’t compile.

Database Interaction

In the Scala backend, we use doobie for functional database interaction. We’re fans of it because it lets us write raw SQL with typeclass evidence for proving that we can take things to and from database rows and filters. For the Haskell backend, I tried out postgresql-simple, which was largely similar. Where doobie has Read and Write typeclasses, postgresql-simple has ToRowand FromRow. Each of them provides methods for combining queries with database connections to return results in an effectful context. I’m guessing that’s basically standard fare for functional database access, but doobie and postgresql-simple are my whole experience in that area so far.

There were two interesting differences though. doobie lets you assert that you’re only going to get a single row back from a database query by calling unique, while there’s no way to do this in postgresql-simple. This has pros and cons. It’s nice because applying handlers for unexpected plurality everywhere is sort of annoying. What this looked like in my side project was mapping the listToMaybe function over my results to end up with either one or no results. However, that convenience comes at a cost, since it means you can end up with runtime errors based on the number of rows you get back from the database.

The other big difference was the SQL quasiquoter. What this enabled was SQL syntax highlighting in the raw SQL strings inside my Haskell files:

Intero, a Haskell IDE for Emacs, plus the postgresql-simple quasiquoter allow context-sensitive syntax highlights.

Syntax highlighting in SQL made writing the long SQL strings necessary for some of the database interaction easier to write correctly the first time.

This is a substantially upgraded string interaction from what happens when you interpolate an expression into a string in Scala, and it makes me hopeful that someday powerful and contextual editor integration will be a part of whatever happens with Scala’s quasiquote work.

Testing

Testing stories are similar in http4s and servant for API testing. Everything is “just a function” (tm, I’m sure), so you test the function calls and don’t need a running instance of your server to test its API. I don’t have anything public I can point to for testing APIs written in either, but the short docs for http4s show how simple it is in that case. Servant’s testing integration is a bit tighter and more powerful. For example, because the entire route structure is extensively typed (vs. partially typed in the http4s case) it can generate requests using the types the API serves and check some interesting assertions like whether the API returns any non-JSON responses. That’s an odd and interesting direction to take API testing and might offer a way to prove business logic to clients in continuous integration.

The broader question I was interested in was property testing in the presence of effects. In the current backend, our strategy is to call a bunch of .unsafeRunSyncs, evaluating effects as part of the test. This is reasonable probably, since it’s the same strategy cats-effect uses to run that library’s IO tests, but it’s still a bit alarming to see a bunch of unsafeFoo methods flying around. Where we’ve had some problems is in writing readable tests and getting nice output from our tests.

...
assert(insertedScene.visibility == fixedUpSceneCreate.visibility,
       "Visibilities match")
assert(insertedScene.tags == fixedUpSceneCreate.tags, "Tags match")
assert(
  insertedScene.sceneMetadata == fixedUpSceneCreate.sceneMetadata,
  "Scene metadatas match")
assert(insertedScene.name == fixedUpSceneCreate.name, "Names match")
assert(
  insertedScene.tileFootprint == fixedUpSceneCreate.tileFootprint,
  "Tile footprints match")
assert(
  insertedScene.dataFootprint == fixedUpSceneCreate.dataFootprint,
  "Data footprints match")
assert(
  insertedScene.metadataFiles == fixedUpSceneCreate.metadataFiles,
  "Metadata files match")
assert(
  insertedScene.ingestLocation == fixedUpSceneCreate.ingestLocation,
  "Ingest locations match")
assert(
  insertedScene.filterFields == fixedUpSceneCreate.filterFields,
  "Filter fields match")
assert(
  insertedScene.statusFields == fixedUpSceneCreate.statusFields,
  "Status fields match")
true
...

Haskell’s QuickCheck library provides a nice monadicIO function that translates from IO Propertyvalues to simple Property values, but it isn’t clear from following the first few clicks from monadicIO whether I’m chasing my way down to an unsafePerformIO. I doubt it, since monadicIOis just wrapping a call to monadic, which doesn’t know anything about its monad m and therefore shouldn’t be able to assume that it even can unsafely run, but I’m wrong about things all the time.

monadicIO facilitates nice interoperability between the test runner hspec and properties with effects:

main :: IO ()
main = hspec $ do
  describe "some db tests" $ do
    it "should round trip a project through the db" $ do
      property insertRoundTripProp
    it "should get a project successfully" $ do
      property getProjectProp

where insertRoundTripProp is defined as:

insertRoundTripProp :: TestHandle -> TestProjectCreate -> Property
insertRoundTripProp (TestHandle handleFactory) (TestProjectCreate projCreate) = monadicIO $ do
  handle <- run handleFactory
  user <- run User.getUser
  [inserted] <- run $ Project.createProject handle projCreate user
  assert $ Project.description inserted == Project._description projCreate  

The heavy lifting there happens in monadicIO, described above, and run, which lifts an IO a into a PropertyM IO a.

The ease of writing QuickCheck tests with effects made me wonder if there’s something nicer we could use in scala, and it looks like with specs2 we can clean up our tests and get better integrated properties and test output. This is an area for future work.

Conclusion

Reimplementing a small part of the Raster Foundry API in Haskell taught me a few things. Both servant and http4s allowed a pleasant separation between the declaration of an API and implementing its concrete interaction with the real world, which facilitates testability. Based on what’s available in Haskell’s web server ecosystem, I was able to find some pointers to future improvements for our Scala development work. Editor integration makes development easier. The more work your types are doing, the less you have to keep in your brain… once you understand the types. But the main lesson was how much non-magical power is available in both ecosystems if you spend the time to learn how the pieces fit together.